AsnShipperService.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  3. using Admin.NET.Plugin.AiDOP.Universal;
  4. /// <summary>
  5. /// 订单发货服务 🚚
  6. /// 路由前缀:/api/Order/asnshipper/...
  7. /// </summary>
  8. [ApiDescriptionSettings(Order = 280, Description = "订单发货")]
  9. [Route("api/Order")]
  10. [AllowAnonymous]
  11. [NonUnify]
  12. public class AsnShipperService : IDynamicApiController, ITransient
  13. {
  14. private readonly ISqlSugarClient _db;
  15. private readonly SqlSugarRepository<AsnShipperMaster> _masterRep;
  16. private readonly SqlSugarRepository<AsnShipperDetail> _detailRep;
  17. private readonly UserManager _userManager;
  18. public AsnShipperService(
  19. ISqlSugarClient db,
  20. SqlSugarRepository<AsnShipperMaster> masterRep,
  21. SqlSugarRepository<AsnShipperDetail> detailRep,
  22. UserManager userManager)
  23. {
  24. _db = db;
  25. _masterRep = masterRep;
  26. _detailRep = detailRep;
  27. _userManager = userManager;
  28. }
  29. // ══════════════════════════════════════════════════════════════
  30. // 列表 GET /api/Order/asnshipper/list
  31. // ══════════════════════════════════════════════════════════════
  32. /// <summary>获取订单发货分页列表 🚚</summary>
  33. [DisplayName("获取订单发货列表")]
  34. [HttpGet("asnshipper/list")]
  35. public async Task<object> GetAsnShipperList([FromQuery] AsnShipperListInput input)
  36. {
  37. var pars = new List<SugarParameter>();
  38. var conditions = new List<string>
  39. {
  40. "a.shtype = 'SH'",
  41. "a.Typed <> 'S'",
  42. "a.IsActive = 1"
  43. };
  44. if (!string.IsNullOrWhiteSpace(input.Id))
  45. {
  46. conditions.Add("a.Id LIKE @Id");
  47. pars.Add(new SugarParameter("@Id", $"%{input.Id.Trim()}%"));
  48. }
  49. if (!string.IsNullOrWhiteSpace(input.OrdNbr))
  50. {
  51. conditions.Add("a.OrdNbr LIKE @OrdNbr");
  52. pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%"));
  53. }
  54. if (!string.IsNullOrWhiteSpace(input.Department))
  55. {
  56. conditions.Add("(b.Department LIKE @Dept OR b.Descr LIKE @Dept)");
  57. pars.Add(new SugarParameter("@Dept", $"%{input.Department.Trim()}%"));
  58. }
  59. if (!string.IsNullOrWhiteSpace(input.ShipDateFrom))
  60. {
  61. conditions.Add("a.ShipDate >= @ShipDateFrom");
  62. pars.Add(new SugarParameter("@ShipDateFrom", input.ShipDateFrom.Trim()));
  63. }
  64. var whereClause = "WHERE " + string.Join(" AND ", conditions);
  65. var baseSql = $"""
  66. SELECT
  67. a.RecID AS Id,
  68. a.Id AS Id1,
  69. a.OrdNbr,
  70. TRIM(CONCAT(IFNULL(b.Department,''), ' ', IFNULL(b.Descr,''))) AS DepartmentName,
  71. a.ShipDate,
  72. a.Status,
  73. a.Remark
  74. FROM ASNBOLShipperMaster a
  75. LEFT JOIN DepartmentMaster b
  76. ON a.Department = b.Department
  77. {whereClause}
  78. """;
  79. var offset = (input.Page - 1) * input.PageSize;
  80. var total = await _db.Ado.GetIntAsync(
  81. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  82. var list = await _db.Ado.SqlQueryAsync<AsnShipperListRow>(
  83. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  84. pars);
  85. return new { total, page = input.Page, pageSize = input.PageSize, list };
  86. }
  87. // ══════════════════════════════════════════════════════════════
  88. // 详情 GET /api/Order/asnshipper/{id}
  89. // ══════════════════════════════════════════════════════════════
  90. /// <summary>获取订单发货详情(含明细)🚚</summary>
  91. [DisplayName("获取订单发货详情")]
  92. [HttpGet("asnshipper/{id}")]
  93. public async Task<object> GetAsnShipperDetail(int id)
  94. {
  95. var master = await _masterRep.GetFirstAsync(u => u.RecID == id)
  96. ?? throw Oops.Oh("发货单不存在");
  97. // 查询客户名称,用于前端下拉回显"编号+名称"
  98. var soldToName = (await _db.Queryable<CustMaster>()
  99. .Where(u => u.Cust == master.SoldTo)
  100. .Select(u => u.SortName)
  101. .FirstAsync()) ?? string.Empty;
  102. // 查询销售单客户名称,用于前端下拉回显"编号_客户名称"
  103. var ordNbrName = string.IsNullOrWhiteSpace(master.OrdNbr) ? string.Empty
  104. : ((await _db.Queryable<SeOrder>()
  105. .Where(u => u.BillNo == master.OrdNbr)
  106. .Select(u => u.CustomName)
  107. .FirstAsync()) ?? string.Empty);
  108. // 查询部门描述,用于前端下拉回显"编号+描述"
  109. var deptName = string.IsNullOrWhiteSpace(master.Department) ? string.Empty
  110. : ((await _db.Queryable<AdoS0DepartmentMaster>()
  111. .Where(u => u.Department == master.Department)
  112. .Select(u => u.Descr)
  113. .FirstAsync()) ?? string.Empty);
  114. // 主键关联:ASNBOLShipperDetail.ASNBOLShipperRecID = 主表 RecID
  115. var details = await _detailRep.GetListAsync(u => u.AsnShipperRecID == id);
  116. // 兼容历史数据:若外键未写入或曾用错误方式取 RecID,则按业务单号 Id + 销售单号 OrdNbr 兜底
  117. if (details.Count == 0 && !string.IsNullOrWhiteSpace(master.Id) && !string.IsNullOrWhiteSpace(master.OrdNbr))
  118. {
  119. details = await _detailRep.GetListAsync(u =>
  120. u.Id == master.Id && u.OrdNbr == master.OrdNbr);
  121. }
  122. return new
  123. {
  124. recID = master.RecID,
  125. id = master.Id,
  126. ordNbr = master.OrdNbr,
  127. ordNbrName = ordNbrName,
  128. soldTo = master.SoldTo,
  129. soldToName = soldToName,
  130. department = master.Department,
  131. departmentName = deptName,
  132. shipDate = master.ShipDate?.ToString("yyyy-MM-dd"),
  133. status = master.Status ?? string.Empty,
  134. remark = master.Remark,
  135. details = details.OrderBy(d => d.Line).Select(d => new
  136. {
  137. recID = d.RecID,
  138. line = d.Line,
  139. ordNbr = d.OrdNbr,
  140. ordLine = d.OrdLine,
  141. containerItem = d.ContainerItem,
  142. descr = d.Descr,
  143. um = d.UM,
  144. location = d.Location,
  145. lotSerial = d.LotSerial,
  146. qtyToShip = d.QtyToShip,
  147. pickingQty = d.PickingQty,
  148. realQty = d.RealQty,
  149. status = d.Status,
  150. remark = d.Remark,
  151. })
  152. };
  153. }
  154. // ══════════════════════════════════════════════════════════════
  155. // 保存(新增/编辑)POST /api/Order/asnshipper/save
  156. // ══════════════════════════════════════════════════════════════
  157. /// <summary>保存订单发货(新增或编辑)🚚</summary>
  158. [DisplayName("保存订单发货")]
  159. [ApiDescriptionSettings(Name = "SaveAsnShipper"), HttpPost("asnshipper/save")]
  160. public async Task<object> SaveAsnShipper([FromBody] AsnShipperSaveInput input)
  161. {
  162. var now = DateTime.Now;
  163. var user = _userManager.Account ?? "system";
  164. if (input.RecID is null or 0)
  165. {
  166. // ── 新增:参照 SysJobService.AddJobDetail ──
  167. // 若发货单号为空则自动生成 SH+当前日期+四位流水号
  168. var shipId = input.Id;
  169. if (string.IsNullOrWhiteSpace(shipId))
  170. {
  171. shipId = await GenerateShipIdAsync(_userManager.TenantId);
  172. }
  173. var entity = new AsnShipperMaster
  174. {
  175. Id = shipId,
  176. OrdNbr = input.OrdNbr,
  177. SoldTo = input.SoldTo,
  178. Department = input.Department,
  179. ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate),
  180. Status = input.Status,
  181. Remark = input.Remark,
  182. ShType = "SH",
  183. Typed = string.Empty,
  184. IsActive = 1,
  185. IsConfirm = 0,
  186. CreateUser = user,
  187. CreateTime = now,
  188. };
  189. var inserted = await _masterRep.AsInsertable(entity).ExecuteReturnEntityAsync();
  190. var newId = inserted.RecID;
  191. await SaveDetailsAsync(newId, input.Id ?? string.Empty, input.Details, user, now, isNew: true);
  192. return new { id = newId, message = "新增成功" };
  193. }
  194. else
  195. {
  196. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  197. var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID.Value)
  198. ?? throw Oops.Oh("发货单不存在");
  199. entity.Id = input.Id;
  200. entity.OrdNbr = input.OrdNbr;
  201. entity.SoldTo = input.SoldTo;
  202. entity.Department = input.Department;
  203. entity.ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate);
  204. entity.Status = input.Status;
  205. entity.Remark = input.Remark;
  206. entity.UpdateUser = user;
  207. entity.UpdateTime = now;
  208. await _masterRep.UpdateAsync(entity);
  209. await SaveDetailsAsync(input.RecID.Value, input.Id ?? string.Empty, input.Details, user, now, isNew: false);
  210. return new { id = input.RecID, message = "编辑成功" };
  211. }
  212. }
  213. /// <summary>
  214. /// 明细三路合并(参照 SeOrderService.SaveEntriesAsync):
  215. /// ① DB有且入参有 → 更新
  216. /// ② DB无入参有 → 新增
  217. /// ③ DB有入参无 → 删除
  218. /// </summary>
  219. private async Task SaveDetailsAsync(int masterRecId, string shipperId,
  220. List<AsnShipperDetailInput> details, string user, DateTime now, bool isNew)
  221. {
  222. var dbDetails = isNew ? new List<AsnShipperDetail>()
  223. : await _detailRep.GetListAsync(u => u.AsnShipperRecID == masterRecId);
  224. var dbById = dbDetails.ToDictionary(u => u.RecID);
  225. var inputIds = new HashSet<int>(details.Where(d => d.RecID is > 0).Select(d => d.RecID!.Value));
  226. for (var i = 0; i < details.Count; i++)
  227. {
  228. var d = details[i];
  229. var lineNo = d.Line ?? (i + 1);
  230. if (d.RecID > 0 && dbById.TryGetValue(d.RecID!.Value, out var existing))
  231. {
  232. // ① 更新
  233. existing.Line = lineNo;
  234. existing.OrdNbr = d.OrdNbr;
  235. existing.OrdLine = d.OrdLine;
  236. existing.ContainerItem = d.ContainerItem;
  237. existing.Descr = d.Descr;
  238. existing.UM = d.UM;
  239. existing.Location = d.Location;
  240. existing.LotSerial = d.LotSerial;
  241. existing.QtyToShip = d.QtyToShip;
  242. existing.PickingQty = d.PickingQty;
  243. existing.RealQty = d.RealQty;
  244. existing.Status = d.Status;
  245. existing.Remark = d.Remark;
  246. existing.UpdateUser = user;
  247. existing.UpdateTime = now;
  248. await _detailRep.UpdateAsync(existing);
  249. }
  250. else
  251. {
  252. // ② 新增
  253. var detail = new AsnShipperDetail
  254. {
  255. Id = shipperId,
  256. AsnShipperRecID = masterRecId,
  257. Line = lineNo,
  258. OrdNbr = d.OrdNbr,
  259. OrdLine = d.OrdLine,
  260. ContainerItem = d.ContainerItem,
  261. Descr = d.Descr,
  262. UM = d.UM,
  263. Location = d.Location,
  264. LotSerial = d.LotSerial,
  265. QtyToShip = d.QtyToShip,
  266. PickingQty = d.PickingQty,
  267. RealQty = d.RealQty,
  268. Status = d.Status,
  269. Remark = d.Remark,
  270. ShType = "SH",
  271. IsActive = 1,
  272. CreateUser = user,
  273. CreateTime = now,
  274. };
  275. await _detailRep.InsertAsync(detail);
  276. }
  277. }
  278. // ③ 删除:DB有、入参无
  279. foreach (var toDelete in dbDetails.Where(u => !inputIds.Contains(u.RecID)))
  280. {
  281. await _detailRep.DeleteAsync(u => u.RecID == toDelete.RecID);
  282. }
  283. }
  284. // ══════════════════════════════════════════════════════════════
  285. // 删除 POST /api/Order/asnshipper/delete
  286. // ══════════════════════════════════════════════════════════════
  287. /// <summary>删除发货单(状态非关闭才可删除)🚚</summary>
  288. [DisplayName("删除订单发货")]
  289. [ApiDescriptionSettings(Name = "DeleteAsnShipper"), HttpPost("asnshipper/delete")]
  290. public async Task<object> DeleteAsnShipper([FromBody] AsnShipperDeleteInput input)
  291. {
  292. // 参照 SysJobService.DeleteJobDetail:先查再删
  293. var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID)
  294. ?? throw Oops.Oh("发货单不存在");
  295. if (entity.Status == "C")
  296. throw Oops.Oh("已关闭的发货单不允许删除");
  297. await _detailRep.DeleteAsync(u => u.AsnShipperRecID == input.RecID);
  298. await _masterRep.DeleteAsync(u => u.RecID == input.RecID);
  299. return new { message = "删除成功" };
  300. }
  301. // ══════════════════════════════════════════════════════════════
  302. // 下拉数据源 GET /api/Order/asnshipper/options/salesords
  303. // ══════════════════════════════════════════════════════════════
  304. /// <summary>获取销售单下拉列表 🚚</summary>
  305. [DisplayName("获取销售单下拉")]
  306. [HttpGet("asnshipper/options/salesords")]
  307. public async Task<object> GetSalesOrdOptions([FromQuery] string? keyword)
  308. {
  309. var pars = new List<SugarParameter>();
  310. var where = "s.IsDeleted = 0";
  311. if (!string.IsNullOrWhiteSpace(keyword))
  312. {
  313. where += " AND (s.bill_no LIKE @kw OR s.custom_name LIKE @kw)";
  314. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  315. }
  316. // MySQL:字符串拼接用 CONCAT(对应 SQL Server 的 bill_no+'_'+custom_name)
  317. var sql = $"""
  318. SELECT s.bill_no AS Value,
  319. CONCAT(s.bill_no, '_', IFNULL(s.custom_name, '')) AS Label
  320. FROM crm_seorder s
  321. WHERE {where}
  322. ORDER BY s.bill_no DESC
  323. LIMIT 100
  324. """;
  325. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  326. return list;
  327. }
  328. // ══════════════════════════════════════════════════════════════
  329. // 下拉数据源 GET /api/Order/asnshipper/options/customers
  330. // ══════════════════════════════════════════════════════════════
  331. /// <summary>获取客户下拉列表 🚚</summary>
  332. [DisplayName("获取客户下拉")]
  333. [HttpGet("asnshipper/options/customers")]
  334. public async Task<object> GetCustomerOptions([FromQuery] string? keyword)
  335. {
  336. var pars = new List<SugarParameter>();
  337. var where = "1=1";
  338. if (!string.IsNullOrWhiteSpace(keyword))
  339. {
  340. where += " AND (Cust LIKE @kw OR SortName LIKE @kw)";
  341. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  342. }
  343. var sql = $"""
  344. SELECT Cust AS Value,
  345. TRIM(CONCAT(Cust, ' ', IFNULL(SortName,''))) AS Label
  346. FROM CustMaster
  347. WHERE {where}
  348. ORDER BY Cust
  349. LIMIT 100
  350. """;
  351. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  352. return list;
  353. }
  354. // ══════════════════════════════════════════════════════════════
  355. // 下拉数据源 GET /api/Order/asnshipper/options/departments
  356. // ══════════════════════════════════════════════════════════════
  357. /// <summary>获取部门下拉列表 🚚</summary>
  358. [DisplayName("获取部门下拉")]
  359. [HttpGet("asnshipper/options/departments")]
  360. public async Task<object> GetDepartmentOptions([FromQuery] string? keyword)
  361. {
  362. var pars = new List<SugarParameter>();
  363. var where = "1=1";
  364. if (!string.IsNullOrWhiteSpace(keyword))
  365. {
  366. where += " AND (Department LIKE @kw OR Descr LIKE @kw)";
  367. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  368. }
  369. var sql = $"""
  370. SELECT Department AS Value,
  371. TRIM(CONCAT(Department, ' ', IFNULL(Descr,''))) AS Label
  372. FROM DepartmentMaster
  373. WHERE {where}
  374. ORDER BY Department
  375. LIMIT 100
  376. """;
  377. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  378. return list;
  379. }
  380. // ──────────────── 号生成 ────────────────
  381. /// <summary>
  382. /// 生成发货单号:SH + yyyyMMdd + 4位流水号,例如 SH202605210001
  383. /// </summary>
  384. private async Task<string> GenerateShipIdAsync(long tenantId)
  385. {
  386. var today = DateTime.Now.ToString("yyyyMMdd");
  387. var prefix = $"SH{today}";
  388. // 查询当天已有的最大发货单号
  389. var maxId = await _db.Queryable<AsnShipperMaster>()
  390. .Where(u => u.Id!.StartsWith(prefix) && u.IsActive == 1 && u.TenantId == tenantId)
  391. .OrderBy(u => u.Id, OrderByType.Desc)
  392. .Select(u => u.Id)
  393. .FirstAsync();
  394. if (string.IsNullOrEmpty(maxId))
  395. return $"{prefix}0001";
  396. // 取后4位转为数字并加1,不足4位补零
  397. var last4 = maxId.Length >= 4 ? maxId[^4..] : maxId;
  398. if (int.TryParse(last4, out var serial))
  399. return $"{prefix}{(serial + 1).ToString().PadLeft(4, '0')}";
  400. return $"{prefix}0001";
  401. }
  402. // ──────────────── 内部查询结果映射类 ────────────────
  403. private sealed class AsnShipperListRow
  404. {
  405. public int Id { get; set; }
  406. public string? Id1 { get; set; }
  407. public string? OrdNbr { get; set; }
  408. public string? DepartmentName { get; set; }
  409. public DateTime? ShipDate { get; set; }
  410. public string? Status { get; set; }
  411. public string? Remark { get; set; }
  412. }
  413. private sealed class SimpleKvRow
  414. {
  415. public string? Value { get; set; }
  416. public string? Label { get; set; }
  417. }
  418. }