using Dm.filter; using MailKit.Search; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using WIDESEA_Common.LocationEnum; using WIDESEA_Common.OrderEnum; using WIDESEA_Common.StockEnum; using WIDESEA_Common.TaskEnum; using WIDESEA_Core; using WIDESEA_Core.BaseRepository; using WIDESEA_Core.BaseServices; using WIDESEA_Core.Helper; using WIDESEA_DTO.Basic; using WIDESEA_DTO.Inbound; using WIDESEA_DTO.Outbound; using WIDESEA_IBasicService; using WIDESEA_IOutboundService; using WIDESEA_IStockService; using WIDESEA_Model.Models; namespace WIDESEA_OutboundService { /// /// /// public class OutboundPickingService : ServiceBase>, IOutboundPickingService { private readonly IUnitOfWorkManage _unitOfWorkManage; public IRepository Repository => BaseDal; private readonly IStockInfoService _stockInfoService; private readonly IStockService _stockService; private readonly IOutStockLockInfoService _outStockLockInfoService; private readonly IStockInfoDetailService _stockInfoDetailService; private readonly ILocationInfoService _locationInfoService; private readonly IOutboundOrderDetailService _outboundOrderDetailService; private readonly IOutboundOrderService _outboundOrderService; private readonly ISplitPackageService _splitPackageService; private readonly IRepository _taskRepository; private readonly IESSApiService _eSSApiService; private readonly IInvokeMESService _invokeMESService; private readonly IDailySequenceService _dailySequenceService; private readonly ILogger _logger; private Dictionary stations = new Dictionary { {"2-1","2-9" }, {"3-1","3-9" }, }; private Dictionary movestations = new Dictionary { {"2-1","2-5" }, {"3-1","3-5" }, }; public OutboundPickingService(IRepository BaseDal, IUnitOfWorkManage unitOfWorkManage, IStockInfoService stockInfoService, IStockService stockService, IOutStockLockInfoService outStockLockInfoService, IStockInfoDetailService stockInfoDetailService, ILocationInfoService locationInfoService, IOutboundOrderDetailService outboundOrderDetailService, ISplitPackageService splitPackageService, IOutboundOrderService outboundOrderService, IRepository taskRepository, IESSApiService eSSApiService, ILogger logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService) : base(BaseDal) { _unitOfWorkManage = unitOfWorkManage; _stockInfoService = stockInfoService; _stockService = stockService; _outStockLockInfoService = outStockLockInfoService; _stockInfoDetailService = stockInfoDetailService; _locationInfoService = locationInfoService; _outboundOrderDetailService = outboundOrderDetailService; _splitPackageService = splitPackageService; _outboundOrderService = outboundOrderService; _taskRepository = taskRepository; _eSSApiService = eSSApiService; _logger = logger; _invokeMESService = invokeMESService; _dailySequenceService = dailySequenceService; } #region 查询出库详情列表 public async Task> GetOutStockLockListAsync(string orderNo) { var locks = await _outStockLockInfoService.Db.Queryable() .Where(t => t.OrderNo == orderNo) .ToListAsync(); return locks.Select(t => new OutStockLockListResp { Id = t.Id, // TaskNum = t.TaskNum, PalletCode = t.PalletCode, CurrentBarcode = t.CurrentBarcode, AssignQuantity = t.AssignQuantity, PickedQty = t.PickedQty, Status = t.Status, // IsSplitted = t.IsSplitted }).ToList(); } #endregion public async Task ValidateBarcode(string barcode) { try { if (string.IsNullOrEmpty(barcode)) { return WebResponseContent.Instance.Error("条码不能为空"); } // 根据条码查询库存明细 var stockDetail = await _stockInfoDetailService.Db.Queryable() .Includes(x => x.StockInfo) .Where(x => x.Barcode == barcode) .FirstAsync(); if (stockDetail == null) { return WebResponseContent.Instance.Error("条码不存在"); } var result = new { Barcode = barcode, MaterielCode = stockDetail.MaterielCode, BatchNo = stockDetail.BatchNo, AvailableQuantity = stockDetail.StockQuantity - stockDetail.OutboundQuantity, LocationCode = stockDetail.StockInfo?.LocationCode, PalletCode = stockDetail.StockInfo?.PalletCode }; return WebResponseContent.Instance.OK(null, result); } catch (Exception ex) { return WebResponseContent.Instance.Error($"条码验证失败: {ex.Message}"); } } // 检查并更新订单状态 private async Task CheckAndUpdateOrderStatus(string orderNo) { var orderDetails = _stockInfoDetailService.Db.Queryable() .LeftJoin((o, item) => o.OrderId == item.Id) // 关联条件:父表 Id = 子表 OrderId .Where((o, item) => item.OrderNo == orderNo) // 过滤父表 OrderNo .Select((o, item) => o) // 只返回子表数据 .ToList(); //var orderDetails = await _stockInfoDetailService.Db.Queryable() // .Where(x => x.OrderId == orderNo.ObjToInt()) // .ToListAsync(); bool allCompleted = true; foreach (var detail in orderDetails) { if (detail.OverOutQuantity < detail.NeedOutQuantity) { allCompleted = false; break; } } if (allCompleted) { try { await _outboundOrderService.Db.Updateable() .SetColumns(x => x.OrderStatus == 2) // 已完成 .Where(x => x.OrderNo == orderNo) .ExecuteCommandAsync(); var outboundOrder = _stockInfoService.Db.Queryable().First(x => x.OrderNo == orderNo); if (outboundOrder != null && outboundOrder.OrderStatus == OutOrderStatusEnum.出库完成.ObjToInt()) { if (outboundOrder.OrderType == OutOrderTypeEnum.Allocate.ObjToInt().ObjToInt())//调拨出库 { } else if (outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt()) //重检出库 { } else { var feedmodel = new FeedbackOutboundRequestModel { reqCode = Guid.NewGuid().ToString(), reqTime = DateTime.Now.ToString(), business_type = outboundOrder.BusinessType, factoryArea = outboundOrder.FactoryArea, operationType = 1, Operator = outboundOrder.Operator, orderNo = outboundOrder.UpperOrderNo, status = outboundOrder.OrderStatus, details = new List() }; var lists = _outStockLockInfoService.Db.Queryable().Where(x => x.OrderNo == orderNo).ToList(); var groupedData = lists.GroupBy(item => new { item.MaterielCode, item.lineNo, item.Unit, item.WarehouseCode }) .Select(group => new FeedbackOutboundDetailsModel { materialCode = group.Key.MaterielCode, lineNo = group.Key.lineNo, warehouseCode = group.Key.WarehouseCode, currentDeliveryQty = group.Sum(x => x.OrderQuantity), // warehouseCode= "1072", unit = group.Key.Unit, barcodes = group.Select(row => new WIDESEA_DTO.Outbound.BarcodesModel { barcode = row.CurrentBarcode, supplyCode = row.SupplyCode, batchNo = row.BatchNo, unit = row.Unit, qty = row.AssignQuantity }).ToList() }).ToList(); feedmodel.details = groupedData; _invokeMESService.FeedbackOutbound(feedmodel); } } } catch (Exception ex) { _logger.LogError(" OutboundPickingService FeedbackOutbound : " + ex.Message); } } } public async Task ConfirmPicking(string orderNo, string palletCode, string barcode) { try { _unitOfWorkManage.BeginTran(); // 1. 查找出库锁定信息 var lockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.Status == (int)OutLockStockStatusEnum.出库中 && it.PalletCode == palletCode && it.CurrentBarcode == barcode) .FirstAsync(); if (lockInfo == null) { lockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.CurrentBarcode == barcode && it.Status == (int)OutLockStockStatusEnum.出库中) .FirstAsync(); if (lockInfo == null) throw new Exception($"条码{barcode}不属于托盘{palletCode}或不存在待分拣记录"); } if (lockInfo.PalletCode != palletCode) throw new Exception($"条码{barcode}不属于托盘{palletCode}"); var outorderdetail = _outboundOrderDetailService.Db.Queryable().First(x => x.Id == lockInfo.OrderDetailId); if (outorderdetail != null && lockInfo.AssignQuantity > outorderdetail.OrderQuantity) { throw new Exception($"条码{barcode}的出库数量大于订单的数量"); } var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(x => x.Barcode == barcode && x.StockId == lockInfo.StockId) .FirstAsync(); if (stockDetail == null) return WebResponseContent.Instance.Error("无效的条码或物料编码"); decimal actualQty = lockInfo.AssignQuantity - lockInfo.PickedQty; decimal stockQuantity = stockDetail.StockQuantity; List splitResults = new List(); if (actualQty < stockQuantity) { // 情况1: 分配数量小于库存数量,需要自动拆包 // 计算剩余库存数量 decimal remainingStockQty = stockQuantity - actualQty; // 更新原条码库存为剩余数量 stockDetail.StockQuantity = remainingStockQty; stockDetail.OutboundQuantity = remainingStockQty; await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); // 生成新条码用于记录拣选数量(但不创建库存记录) var seq = await _dailySequenceService.GetNextSequenceAsync(); string newBarcode = "WSLOT" + DateTime.Now.ToString("yyyyMMdd") + seq.ToString()?.PadLeft(5, '0'); // 为新条码创建出库锁定信息(用于记录拣选) var newLockInfo = new Dt_OutStockLockInfo { OrderNo = lockInfo.OrderNo, OrderDetailId = lockInfo.OrderDetailId, BatchNo = lockInfo.BatchNo, MaterielCode = lockInfo.MaterielCode, MaterielName = lockInfo.MaterielName, StockId = lockInfo.StockId, OrderQuantity = actualQty, OriginalQuantity = actualQty, AssignQuantity = actualQty, PickedQty = actualQty, LocationCode = lockInfo.LocationCode, PalletCode = lockInfo.PalletCode, TaskNum = lockInfo.TaskNum, Status = (int)OutLockStockStatusEnum.拣选完成, Unit = lockInfo.Unit, SupplyCode = lockInfo.SupplyCode, OrderType = lockInfo.OrderType, CurrentBarcode = newBarcode, OriginalLockQuantity = actualQty, IsSplitted = 1, ParentLockId = lockInfo.Id }; await _outStockLockInfoService.Db.Insertable(newLockInfo).ExecuteCommandAsync(); // 记录拆包历史(用于追踪) var splitHistory = new Dt_SplitPackageRecord { FactoryArea = lockInfo.FactoryArea, TaskNum = lockInfo.TaskNum, OutStockLockInfoId = lockInfo.Id, StockId = stockDetail.StockId, Operator = App.User.UserName, IsReverted = false, OriginalBarcode = barcode, NewBarcode = newBarcode, SplitQty = actualQty, RemainQuantity = remainingStockQty, MaterielCode = lockInfo.MaterielCode, SplitTime = DateTime.Now, OrderNo = lockInfo.OrderNo, PalletCode = lockInfo.PalletCode, Status = (int)SplitPackageStatusEnum.已拣选 }; await _splitPackageService.Db.Insertable(splitHistory).ExecuteCommandAsync(); // 更新原锁定信息为剩余库存数量 lockInfo.AssignQuantity = remainingStockQty; lockInfo.PickedQty = 0; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); splitResults.Add(new SplitResult { OriginalBarcode = barcode, NewBarcode = newBarcode, SplitQuantity = actualQty, RemainQuantity = remainingStockQty }); // 更新拣选记录中的条码为新条码 barcode = newBarcode; lockInfo = newLockInfo; } else if (actualQty == stockQuantity) { // 情况2: 分配数量等于库存数量,整包出库 stockDetail.StockQuantity = 0; stockDetail.OutboundQuantity = 0; await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); lockInfo.PickedQty += actualQty; lockInfo.Status = (int)OutLockStockStatusEnum.拣选完成; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); } else { // 情况3: 分配数量大于库存数量,库存整包出库 // 整包出库当前库存 decimal stockOutQty = stockQuantity; stockDetail.StockQuantity = 0; stockDetail.OutboundQuantity = 0; await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); // 计算剩余分配数量 decimal remainingAssignQty = actualQty - stockQuantity; // 更新锁定信息(只完成库存部分) lockInfo.PickedQty += stockOutQty; lockInfo.AssignQuantity = remainingAssignQty; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); var _relatedSplitRecords = await _splitPackageService.Db.Queryable() .Where(it => it.OriginalBarcode == barcode || it.NewBarcode == barcode) .Where(it => !it.IsReverted) .ToListAsync(); foreach (var record in _relatedSplitRecords) { record.Status = (int)SplitPackageStatusEnum.已拣选; await _splitPackageService.Db.Updateable(record).ExecuteCommandAsync(); } } await _outboundOrderDetailService.Db.Updateable() .SetColumns(it => it.PickedQty == it.PickedQty + actualQty) .Where(it => it.Id == lockInfo.OrderDetailId) .ExecuteCommandAsync(); await CheckAndUpdateOrderStatus(orderNo); // 查询任务表 var task = _taskRepository.QueryData(x => x.OrderNo == orderNo && x.PalletCode == palletCode).FirstOrDefault(); // 记录拣选历史 var pickingHistory = new Dt_PickingRecord { FactoryArea = lockInfo.FactoryArea, TaskNo = task?.TaskNum ?? 0, LocationCode = task?.SourceAddress ?? "", StockId = stockDetail.Id, OrderNo = orderNo, OrderDetailId = lockInfo.OrderDetailId, PalletCode = palletCode, Barcode = barcode, MaterielCode = lockInfo.MaterielCode, PickQuantity = actualQty, PickTime = DateTime.Now, Operator = App.User.UserName, OutStockLockId = lockInfo.Id }; await Db.Insertable(pickingHistory).ExecuteCommandAsync(); _unitOfWorkManage.CommitTran(); // 如果有拆包结果,返回拆包信息 if (splitResults.Any()) { return WebResponseContent.Instance.OK("拣选确认成功,已自动拆包", new { SplitResults = splitResults }); } return WebResponseContent.Instance.OK("拣选确认成功"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); return WebResponseContent.Instance.Error($"拣选确认失败:{ex.Message}"); } } /// /// 回库操作 /// public async Task ReturnRemaining(string orderNo, string palletCode, string reason) { try { _unitOfWorkManage.BeginTran(); // 获取所有未分拣的出库锁定记录,包括拆包产生的记录 var remainingLocks = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.Status == (int)OutLockStockStatusEnum.出库中) .ToListAsync(); var stockinfo = _stockInfoService.Db.Queryable().First(x => x.PalletCode == palletCode); var tasks = new List(); // 查询任务表 var task = remainingLocks.Any() ? _taskRepository.QueryData(x => x.TaskNum == remainingLocks.First().TaskNum).FirstOrDefault() : _taskRepository.QueryData(x => x.PalletCode == palletCode).FirstOrDefault(); if (task == null) { return WebResponseContent.Instance.Error("未找到对应的任务信息"); } // 检查托盘上是否有其他非出库货物(库存货物) var palletStockGoods = await _stockInfoDetailService.Db.Queryable() .Where(it => it.StockId == stockinfo.Id && (it.Status == StockStatusEmun.入库确认.ObjToInt() || it.Status == StockStatusEmun.入库完成.ObjToInt() || it.Status == StockStatusEmun.出库锁定.ObjToInt())) .Where(it => it.OutboundQuantity == 0 || it.OutboundQuantity < it.StockQuantity) // 未完全出库的 .ToListAsync(); // 检查拆包记录,找出需要回库的条码 var splitRecords = await _splitPackageService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && !it.IsReverted) .ToListAsync(); // 计算需要回库的拆包条码 var splitBarcodesToReturn = new List(); foreach (var splitRecord in splitRecords) { // 检查原条码是否还有库存需要回库 var originalStock = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == splitRecord.OriginalBarcode && it.StockId == stockinfo.Id) .FirstAsync(); if (originalStock != null && originalStock.StockQuantity > 0) { splitBarcodesToReturn.Add(splitRecord.OriginalBarcode); } // 检查新条码是否还有库存需要回库 var newStock = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == splitRecord.NewBarcode && it.StockId == stockinfo.Id) .FirstAsync(); if (newStock != null && newStock.StockQuantity > 0) { splitBarcodesToReturn.Add(splitRecord.NewBarcode); } } // 如果没有需要回库的货物(既无未分拣出库货物,也无其他库存货物,也无拆包剩余货物) if (!remainingLocks.Any() && !palletStockGoods.Any() && !splitBarcodesToReturn.Any()) { // 检查是否所有货物都已拣选完成 var allPicked = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode) .AnyAsync(it => it.Status == (int)OutLockStockStatusEnum.拣选完成); if (allPicked) { return WebResponseContent.Instance.OK("所有货物已拣选完成,托盘为空"); } else { return WebResponseContent.Instance.Error("没有需要回库的剩余货物"); } } var firstlocation = _locationInfoService.Db.Queryable().First(x => x.LocationCode == task.SourceAddress); decimal totalReturnQty = 0; // 情况1:处理未分拣的出库锁定记录 if (remainingLocks.Any(x => x.PalletCode == palletCode)) { var palletLocks = remainingLocks.Where(x => x.PalletCode == palletCode).ToList(); totalReturnQty = palletLocks.Sum(x => x.AssignQuantity - x.PickedQty); if (totalReturnQty > 0) { // 分配新货位 var newLocation = _locationInfoService.AssignLocation(firstlocation.LocationType); // 更新出库锁定记录状态 var lockIds = palletLocks.Select(x => x.Id).ToList(); await _outStockLockInfoService.Db.Updateable() .SetColumns(it => new Dt_OutStockLockInfo { Status = (int)OutLockStockStatusEnum.回库中 }) .Where(it => lockIds.Contains(it.Id)) .ExecuteCommandAsync(); // 处理库存记录 foreach (var lockInfo in palletLocks) { decimal returnQty = lockInfo.AssignQuantity - lockInfo.PickedQty; // 检查库存记录是否存在 var existingStock = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == lockInfo.CurrentBarcode && it.StockId == lockInfo.StockId) .FirstAsync(); if (existingStock != null) { // 库存记录存在,恢复锁定数量 existingStock.OutboundQuantity = 0; await _stockInfoDetailService.Db.Updateable(existingStock).ExecuteCommandAsync(); } else { // 库存记录不存在(可能是拆包产生的新条码),创建新的库存记录 var newStockDetail = new Dt_StockInfoDetail { StockId = lockInfo.StockId, MaterielCode = lockInfo.MaterielCode, MaterielName = lockInfo.MaterielName, OrderNo = lockInfo.OrderNo, BatchNo = lockInfo.BatchNo, StockQuantity = returnQty, OutboundQuantity = 0, Barcode = lockInfo.CurrentBarcode, InboundOrderRowNo = "", Status = StockStatusEmun.入库完成.ObjToInt(), SupplyCode = lockInfo.SupplyCode, WarehouseCode = lockInfo.WarehouseCode, Unit = lockInfo.Unit }; await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync(); } } // 创建回库任务 CreateReturnTask(tasks, task, palletCode, newLocation); } } // 情况2:处理拆包剩余的库存货物 if (splitBarcodesToReturn.Any()) { decimal splitReturnQty = 0; foreach (var barcode in splitBarcodesToReturn) { var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == barcode && it.StockId == stockinfo.Id) .FirstAsync(); if (stockDetail != null && stockDetail.StockQuantity > 0) { splitReturnQty += stockDetail.StockQuantity; // 恢复库存状态为入库完成 stockDetail.OutboundQuantity = 0; await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); } } totalReturnQty += splitReturnQty; // 如果没有创建任务,创建回库任务 if (!tasks.Any()) { var newLocation = _locationInfoService.AssignLocation(firstlocation.LocationType); CreateReturnTask(tasks, task, palletCode, newLocation); } } // 情况3:出库货物已分拣完,但托盘上还有其他库存货物需要回库 if (palletStockGoods.Any() && !remainingLocks.Any(x => x.PalletCode == palletCode)) { decimal otherReturnQty = palletStockGoods.Sum(x => x.StockQuantity - x.OutboundQuantity); totalReturnQty += otherReturnQty; // 更新这些库存货物的状态 foreach (var stockGood in palletStockGoods) { stockGood.OutboundQuantity = 0; await _stockInfoDetailService.Db.Updateable(stockGood).ExecuteCommandAsync(); } // 如果没有创建任务,创建回库任务 if (!tasks.Any()) { var newLocation = _locationInfoService.AssignLocation(firstlocation.LocationType); CreateReturnTask(tasks, task, palletCode, newLocation); } } var allSplitRecords = await _splitPackageService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && !it.IsReverted) .ToListAsync(); foreach (var record in allSplitRecords) { record.Status = (int)SplitPackageStatusEnum.已回库; await _splitPackageService.Db.Updateable(record).ExecuteCommandAsync(); } // 保存任务 给ESS下发任务 if (tasks.Any()) { try { await _taskRepository.Db.Insertable(tasks).ExecuteCommandAsync(); var targetAddress = task.TargetAddress; _taskRepository.DeleteData(task); // 给 ESS 流动信号和创建任务 try { var result = await _eSSApiService.MoveContainerAsync(new WIDESEA_DTO.Basic.MoveContainerRequest { slotCode = movestations[targetAddress], containerCode = palletCode }); if (result) { TaskModel esstask = new TaskModel() { taskType = "putaway", taskGroupCode = "", groupPriority = 0, tasks = new List { new() { taskCode = tasks.First().TaskNum.ToString(), taskPriority = 0, taskDescribe = new TaskDescribeType { containerCode = palletCode, containerType = "CT_KUBOT_STANDARD", fromLocationCode = stations.GetValueOrDefault(targetAddress) ?? "", toStationCode = "", toLocationCode = tasks.First().TargetAddress, deadline = 0, storageTag = "" } } } }; var resulttask = await _eSSApiService.CreateTaskAsync(esstask); _logger.LogInformation("ReturnRemaining 创建任务返回: " + resulttask); } } catch (Exception ex) { _logger.LogInformation("ReturnRemaining 创建任务返回 catch err: " + ex.Message); } _unitOfWorkManage.CommitTran(); return WebResponseContent.Instance.OK($"回库操作成功,共回库数量:{totalReturnQty}"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); return WebResponseContent.Instance.Error($"创建回库任务失败: {ex.Message}"); } } _unitOfWorkManage.RollbackTran(); return WebResponseContent.Instance.Error("未创建任何回库任务"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); return WebResponseContent.Instance.Error($"回库操作失败: {ex.Message}"); } } /// /// 创建回库任务 /// private void CreateReturnTask(List tasks, Dt_Task originalTask, string palletCode, Dt_LocationInfo newLocation) { Dt_Task newTask = new() { CurrentAddress = stations[originalTask.TargetAddress], Grade = 0, PalletCode = palletCode, NextAddress = "", OrderNo = originalTask.OrderNo, Roadway = newLocation.RoadwayNo, SourceAddress = stations[originalTask.TargetAddress], TargetAddress = newLocation.LocationCode, TaskStatus = TaskStatusEnum.New.ObjToInt(), TaskType = TaskTypeEnum.InPick.ObjToInt(), PalletType = originalTask.PalletType, WarehouseId = originalTask.WarehouseId, }; tasks.Add(newTask); } /// /// 检查托盘是否需要回库的辅助方法 /// public async Task CheckPalletNeedReturn(string orderNo, string palletCode) { // 1. 检查是否有未分拣的出库记录 var hasUnpickedLocks = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && it.Status == 1) .AnyAsync(); if (hasUnpickedLocks) return true; // 2. 检查出库是否已完成但托盘还有库存货物 var outboundFinished = !await _outStockLockInfoService.Db.Queryable() .Where(it => it.PalletCode == palletCode && it.Status == 1) .AnyAsync(); var stockinfo = _stockInfoService.Db.Queryable().First(x => x.PalletCode == palletCode); var hasRemainingGoods = await _stockInfoDetailService.Db.Queryable() .Where(it => it.StockId == stockinfo.Id && it.Status == StockStatusEmun.入库确认.ObjToInt()) .Where(it => it.OutboundQuantity == 0 || it.OutboundQuantity < it.StockQuantity) .AnyAsync(); return outboundFinished && hasRemainingGoods; } // 取消拣选功能 public async Task CancelPicking(string orderNo, string palletCode, string barcode) { try { _unitOfWorkManage.BeginTran(); //查找拣选记录 var pickingRecord = await Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && it.Barcode == barcode) .OrderByDescending(it => it.PickTime) .FirstAsync(); if (pickingRecord == null) return WebResponseContent.Instance.Error("未找到对应的拣选记录"); // 查找出库锁定信息 var lockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.Id == pickingRecord.OutStockLockId) .FirstAsync(); if (lockInfo == null) return WebResponseContent.Instance.Error("未找到对应的出库锁定信息"); //检查是否可以取消(状态必须是拣选完成) if (lockInfo.Status != (int)OutLockStockStatusEnum.拣选完成) return WebResponseContent.Instance.Error("当前状态不允许取消分拣"); decimal cancelQty = pickingRecord.PickQuantity; // 检查拆包链关系 var splitChain = await GetSplitChain(barcode); if (splitChain.Any()) { // 情况A:处理拆包链的取消(多次手动拆包) await HandleSplitChainCancel(orderNo, palletCode, barcode, cancelQty, lockInfo, pickingRecord, splitChain); } else { // 情况B:处理普通条码的取消 await HandleNormalBarcodeCancel(orderNo, palletCode, barcode, cancelQty, lockInfo, pickingRecord); } _unitOfWorkManage.CommitTran(); return WebResponseContent.Instance.OK($"取消分拣成功,恢复数量:{cancelQty}"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); return WebResponseContent.Instance.Error($"取消分拣失败:{ex.Message}"); } } /// /// 获取拆包链(从当前条码追溯到原始条码) /// // 在 GetSplitChain 方法中添加更严格的验证 private async Task> GetSplitChain(string currentBarcode) { var chain = new List(); var visited = new HashSet(); string current = currentBarcode; int maxDepth = 10; // 防止无限循环 while (!string.IsNullOrEmpty(current) && maxDepth > 0) { maxDepth--; if (visited.Contains(current)) { _logger.LogWarning($"检测到循环引用在拆包链中: {current}"); break; } visited.Add(current); var splitRecord = await _splitPackageService.Db.Queryable() .Where(it => it.NewBarcode == current && !it.IsReverted) .FirstAsync(); if (splitRecord == null) break; // 验证拆包记录的完整性 if (string.IsNullOrEmpty(splitRecord.OriginalBarcode)) { _logger.LogError($"拆包记录 {splitRecord.Id} 缺少原始条码"); break; } var item = new SplitChainItem { SplitRecord = splitRecord, OriginalBarcode = splitRecord.OriginalBarcode, NewBarcode = splitRecord.NewBarcode, SplitQuantity = splitRecord.SplitQty }; chain.Add(item); current = splitRecord.OriginalBarcode; } if (maxDepth <= 0) { _logger.LogWarning($"拆包链追溯达到最大深度: {currentBarcode}"); } chain.Reverse(); return chain; } /// /// 处理拆包链的取消分拣 /// private async Task HandleSplitChainCancel(string orderNo, string palletCode, string barcode, decimal cancelQty, Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord, List splitChain) { if (!splitChain.Any()) return; // 找到原始条码(链的第一个) var originalSplitItem = splitChain.First(); var originalBarcode = originalSplitItem.OriginalBarcode; // 查找原始条码的锁定信息和库存 var originalLockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.CurrentBarcode == originalBarcode && it.Status == (int)OutLockStockStatusEnum.出库中) .FirstAsync(); var originalStockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == originalBarcode && it.StockId == originalLockInfo.StockId) .FirstAsync(); if (originalLockInfo == null || originalStockDetail == null) throw new Exception("未找到原始条码的锁定信息或库存信息"); // 恢复原始条码库存(将取消的数量加回去) originalStockDetail.StockQuantity += cancelQty; originalStockDetail.OutboundQuantity += cancelQty; await _stockInfoDetailService.Db.Updateable(originalStockDetail).ExecuteCommandAsync(); // 恢复原始条码锁定信息 originalLockInfo.AssignQuantity += cancelQty; await _outStockLockInfoService.Db.Updateable(originalLockInfo).ExecuteCommandAsync(); // 删除拆包链中所有新条码的锁定信息和库存记录 var allNewBarcodes = splitChain.Select(x => x.NewBarcode).ToList(); // 删除锁定信息 await _outStockLockInfoService.Db.Deleteable() .Where(it => allNewBarcodes.Contains(it.CurrentBarcode)) .ExecuteCommandAsync(); // 删除库存记录(只删除拆包产生的新条码库存,保留原始条码) await _stockInfoDetailService.Db.Deleteable() .Where(it => allNewBarcodes.Contains(it.Barcode) && it.Barcode != originalBarcode) .ExecuteCommandAsync(); // 更新拆包链中所有拆包记录状态为已拆包 foreach (var chainItem in splitChain) { chainItem.SplitRecord.Status = (int)SplitPackageStatusEnum.已拆包; await _splitPackageService.Db.Updateable(chainItem.SplitRecord).ExecuteCommandAsync(); } // 恢复订单明细拣选数量(使用原始锁定信息的订单明细ID) await _outboundOrderDetailService.Db.Updateable() .SetColumns(it => it.PickedQty == it.PickedQty - cancelQty) .Where(it => it.Id == originalLockInfo.OrderDetailId) .ExecuteCommandAsync(); // 恢复订单状态 await CheckAndRevertOrderStatus(orderNo); // 删除拣选记录 await Db.Deleteable() .Where(it => it.Id == pickingRecord.Id) .ExecuteCommandAsync(); //// 记录取消操作历史 //await RecordCancelHistory(orderNo, palletCode, barcode, cancelQty, pickingRecord.Id, // lockInfo.MaterielCode, "取消拆包链分拣"); } /// /// 处理普通条码的取消分拣 /// private async Task HandleNormalBarcodeCancel(string orderNo, string palletCode, string barcode, decimal cancelQty, Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord) { // 1. 查找库存信息 var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == barcode && it.StockId == lockInfo.StockId) .FirstAsync(); if (stockDetail == null) throw new Exception("未找到对应的库存信息"); // 2. 恢复库存数量 if (stockDetail.StockQuantity == 0) { // 整包出库的情况 stockDetail.StockQuantity = cancelQty; stockDetail.OutboundQuantity = cancelQty; } else { // 部分出库的情况 stockDetail.StockQuantity += cancelQty; stockDetail.OutboundQuantity += cancelQty; } await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); // 3. 恢复锁定信息状态 lockInfo.AssignQuantity += cancelQty; lockInfo.PickedQty -= cancelQty; if (lockInfo.PickedQty == 0) { lockInfo.Status = (int)OutLockStockStatusEnum.出库中; } await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); // 4. 处理相关的拆包记录状态恢复 var relatedSplitRecords = await _splitPackageService.Db.Queryable() .Where(it => it.OriginalBarcode == barcode && it.Status == (int)SplitPackageStatusEnum.已拣选) .ToListAsync(); foreach (var record in relatedSplitRecords) { record.Status = (int)SplitPackageStatusEnum.已拆包; await _splitPackageService.Db.Updateable(record).ExecuteCommandAsync(); } // 5. 恢复订单明细的拣选数量 await _outboundOrderDetailService.Db.Updateable() .SetColumns(it => it.PickedQty == it.PickedQty - cancelQty) .Where(it => it.Id == lockInfo.OrderDetailId) .ExecuteCommandAsync(); // 6. 恢复订单状态 await CheckAndRevertOrderStatus(orderNo); // 7. 删除拣选记录 await Db.Deleteable().Where(it => it.Id == pickingRecord.Id).ExecuteCommandAsync(); //// 8. 记录取消操作历史 //await RecordCancelHistory(orderNo, palletCode, barcode, cancelQty, pickingRecord.Id, // lockInfo.MaterielCode, "取消分拣"); } /// /// 检查并恢复订单状态 /// private async Task CheckAndRevertOrderStatus(string orderNo) { var order = await _outboundOrderService.Db.Queryable() .Where(x => x.OrderNo == orderNo) .FirstAsync(); if (order != null && order.OrderStatus == OutOrderStatusEnum.出库完成.ObjToInt()) { await _outboundOrderService.Db.Updateable() .SetColumns(x => x.OrderStatus == OutOrderStatusEnum.出库中.ObjToInt()) .Where(x => x.OrderNo == orderNo) .ExecuteCommandAsync(); } } /// /// 记录取消操作历史 /// private async Task RecordCancelHistory(string orderNo, string palletCode, string barcode, decimal cancelQty, int pickingRecordId, string materielCode, string reason) { //var cancelHistory = new Dt_PickingCancelRecord //{ // OrderNo = orderNo, // PalletCode = palletCode, // Barcode = barcode, // CancelQuantity = cancelQty, // CancelTime = DateTime.Now, // Operator = App.User.UserName, // OriginalPickingRecordId = pickingRecordId, // MaterielCode = materielCode, // Reason = reason //}; //await Db.Insertable(cancelHistory).ExecuteCommandAsync(); } /// /// 拆包链项 /// public class SplitChainItem { public Dt_SplitPackageRecord SplitRecord { get; set; } public string OriginalBarcode { get; set; } public string NewBarcode { get; set; } public decimal SplitQuantity { get; set; } } // 获取未拣选列表 public async Task> GetUnpickedList(string orderNo, string palletCode) { var list = await _outStockLockInfoService.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode && x.Status == 1) .ToListAsync(); return list.Where(x => x.RemainQuantity > 0).ToList(); } // 获取已拣选列表 public async Task> GetPickedList(string orderNo, string palletCode) { var list = await _outStockLockInfoService.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode && x.Status == 6) .ToListAsync(); return list; } // 获取拣选汇总 public async Task GetPickingSummary(ConfirmPickingDto dto) { var picked = await _outStockLockInfoService.Db.Queryable() .WhereIF(!string.IsNullOrEmpty(dto.OrderNo), x => x.OrderNo == dto.OrderNo) .WhereIF(!string.IsNullOrEmpty(dto.PalletCode), x => x.PalletCode == dto.PalletCode) .Where(x => x.Status == 6) .GroupBy(x => new { x.PalletCode, x.MaterielCode }) .Select(x => new SummaryPickingDto { PalletCode = x.PalletCode, MaterielCode = x.MaterielCode, pickedCount = SqlFunc.AggregateCount(x.Id) }).FirstAsync(); if (picked == null) { picked = new SummaryPickingDto { pickedCount = 0 }; } var summary = await _outStockLockInfoService.Db.Queryable() .WhereIF(!string.IsNullOrEmpty(dto.OrderNo), x => x.OrderNo == dto.OrderNo) .WhereIF(!string.IsNullOrEmpty(dto.PalletCode), x => x.PalletCode == dto.PalletCode) .Where(x => x.Status == 1) .GroupBy(x => new { x.PalletCode, x.MaterielCode }) .Select(x => new SummaryPickingDto { PalletCode = x.PalletCode, MaterielCode = x.MaterielCode, UnpickedCount = SqlFunc.AggregateCount(x.Id), UnpickedQuantity = SqlFunc.AggregateSum(x.AssignQuantity) - SqlFunc.AggregateSum(x.PickedQty), }).FirstAsync(); if (summary == null) { summary = new SummaryPickingDto { pickedCount = 0 }; } summary.pickedCount = picked.pickedCount; return summary; } /// /// 获取拣选历史 /// public async Task> GetPickingHistory(int orderId) { // 通过出库单ID查询相关的拣选历史 // 注意:Dt_PickingRecord 中没有直接存储OrderId,需要通过出库单明细关联 var detailIds = await _outboundOrderDetailService.Db.Queryable() .Where(d => d.OrderId == orderId) .Select(d => d.Id) .ToListAsync(); return await Db.Queryable() .Where(p => detailIds.Contains(p.OrderDetailId)) .OrderByDescending(p => p.PickTime) .ToListAsync(); } /// /// 获取托盘的出库状态信息 /// public async Task GetPalletOutboundStatus(string palletCode) { // 获取托盘的锁定信息 var lockInfos = await _outStockLockInfoService.Db.Queryable() .Where(x => x.PalletCode == palletCode) .ToListAsync(); // 获取托盘库存信息 var stockInfo = await _stockInfoService.Db.Queryable() .Includes(x => x.Details) .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (stockInfo == null) return WebResponseContent.Instance.Error("未找到托盘信息"); // 计算各种数量 var totalStockQuantity = stockInfo.Details.Sum(x => x.StockQuantity); var totalOutboundQuantity = stockInfo.Details.Sum(x => x.OutboundQuantity); var totalLockedQuantity = lockInfos.Where(x => x.Status == (int)OutLockStockStatusEnum.出库中) .Sum(x => x.AssignQuantity - x.PickedQty); var totalPickedQuantity = lockInfos.Sum(x => x.PickedQty); var result = new { PalletCode = palletCode, LocationCode = stockInfo.LocationCode, StockStatus = stockInfo.StockStatus, TotalStockQuantity = totalStockQuantity, TotalOutboundQuantity = totalOutboundQuantity, TotalLockedQuantity = totalLockedQuantity, TotalPickedQuantity = totalPickedQuantity, AvailableQuantity = totalStockQuantity - totalOutboundQuantity, LockInfos = lockInfos.Select(x => new { x.Id, x.MaterielCode, x.OrderDetailId, x.AssignQuantity, x.PickedQty, x.Status, x.CurrentBarcode, x.IsSplitted }).ToList(), StockDetails = stockInfo.Details.Select(x => new { x.Barcode, x.MaterielCode, StockQuantity = x.StockQuantity, OutboundQuantity = x.OutboundQuantity, AvailableQuantity = x.StockQuantity - x.OutboundQuantity }).ToList() }; return WebResponseContent.Instance.OK(null, result); } /// /// 直接出库 - 整个托盘出库,清空库存 /// public async Task DirectOutbound(DirectOutboundRequest request) { try { _unitOfWorkManage.BeginTran(); var stockInfo = await _stockInfoService.Db.Queryable() .Includes(x => x.Details) .Where(x => x.PalletCode == request.PalletCode).FirstAsync(); if (stockInfo == null) return WebResponseContent.Instance.Error("未找到托盘库存信息"); var lockInfos = await _outStockLockInfoService.Db.Queryable() .Where(x => x.OrderNo == request.OrderNo && x.PalletCode == request.PalletCode) .ToListAsync(); foreach (var lockInfo in lockInfos) { if (lockInfo.Status == (int)OutLockStockStatusEnum.出库中) { lockInfo.PickedQty = lockInfo.AssignQuantity; } lockInfo.Status = (int)OutLockStockStatusEnum.已出库; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); var orderDetail = await _outboundOrderDetailService.Db.Queryable() .Where(x => x.Id == lockInfo.OrderDetailId) .FirstAsync(); if (orderDetail != null) { orderDetail.OverOutQuantity += lockInfo.PickedQty; orderDetail.LockQuantity -= lockInfo.PickedQty; orderDetail.OrderDetailStatus = (int)OrderDetailStatusEnum.Over; orderDetail.LockQuantity = 0; await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync(); } } var groupDetails = lockInfos.GroupBy(x => x.OrderDetailId).Select(x => new { OrderDetailId = x.Key, TotalQuantity = x.Sum(o => o.PickedQty) }).ToList(); foreach (var item in groupDetails) { var orderDetail = await _outboundOrderDetailService.Db.Queryable().Where(x => x.Id == item.OrderDetailId).FirstAsync(); if (orderDetail != null) { orderDetail.OverOutQuantity = item.TotalQuantity; orderDetail.LockQuantity = 0; orderDetail.OrderDetailStatus = (int)OrderDetailStatusEnum.Over; await _outboundOrderDetailService.Db.Updateable(orderDetail).ExecuteCommandAsync(); } } await CheckAndUpdateOrderStatus(request.OrderNo); var lockInfoIds = lockInfos.Select(x => x.Id).ToList(); var splitRecords = await _splitPackageService.Db.Queryable() .Where(x => lockInfoIds.Contains(x.OutStockLockInfoId) && x.Status == (int)SplitPackageStatusEnum.已拆包) .ToListAsync(); foreach (var record in splitRecords) { record.Status = (int)SplitPackageStatusEnum.已拣选; await _splitPackageService.Db.Updateable(record).ExecuteCommandAsync(); } var location = await _locationInfoService.Db.Queryable() .Where(x => x.LocationCode == stockInfo.LocationCode) .FirstAsync(); if (location != null) { location.LocationStatus = (int)LocationStatusEnum.Free; await _locationInfoService.Db.Updateable(location).ExecuteCommandAsync(); } foreach (var detail in stockInfo.Details) { await _stockInfoDetailService.Db.Deleteable(detail).ExecuteCommandAsync(); } await _stockInfoService.Db.Deleteable(stockInfo).ExecuteCommandAsync(); _unitOfWorkManage.CommitTran(); return WebResponseContent.Instance.OK("直接出库成功"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); return WebResponseContent.Instance.Error($"直接出库失败: {ex.Message}"); } } } }