using Dm.filter; using MailKit.Search; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using WIDESEA_Common.CommonEnum; 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.Enums; 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> 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; } #endregion #region 核心业务流程 /// /// 拣选 /// /// /// /// /// public async Task ConfirmPicking(string orderNo, string palletCode, string barcode) { try { _unitOfWorkManage.BeginTran(); var validationResult = await ValidatePickingRequest(orderNo, palletCode, barcode); if (!validationResult.IsValid) return WebResponseContent.Instance.Error(validationResult.ErrorMessage); var (lockInfo, orderDetail, stockDetail) = validationResult.Data; // 计算实际拣选数量 var quantityResult = await CalculateActualPickingQuantity(lockInfo, orderDetail, stockDetail); if (!quantityResult.IsValid) return WebResponseContent.Instance.Error(quantityResult.ErrorMessage); var (actualQty, adjustedReason) = quantityResult.Data; var overPickingValidation = await ValidateOverPicking(orderDetail.Id, actualQty); if (!overPickingValidation.IsValid) { return WebResponseContent.Instance.Error(overPickingValidation.ErrorMessage); } // 执行分拣逻辑 var pickingResult = await ExecutePickingLogic(lockInfo, orderDetail, stockDetail, orderNo, palletCode, barcode, actualQty); // 记录操作历史 await RecordPickingHistory(pickingResult, orderNo, palletCode); _unitOfWorkManage.CommitTran(); return CreatePickingResponse(pickingResult, adjustedReason); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); _logger.LogError($"ConfirmPicking失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {barcode}, Error: {ex.Message}"); return WebResponseContent.Instance.Error($"拣选确认失败:{ex.Message}"); } } /// /// 取消拣选 /// /// /// /// /// public async Task CancelPicking(string orderNo, string palletCode, string barcode) { try { if (await IsPalletReturned(palletCode)) { return WebResponseContent.Instance.Error($"托盘{palletCode}已经回库,不能取消分拣"); } _unitOfWorkManage.BeginTran(); // 1. 前置验证 var validationResult = await ValidateCancelRequest(orderNo, palletCode, barcode); if (!validationResult.IsValid) return WebResponseContent.Instance.Error(validationResult.ErrorMessage); var (pickingRecord, lockInfo, orderDetail) = validationResult.Data; // 2. 执行取消逻辑 await ExecuteCancelLogic(lockInfo, pickingRecord, orderDetail, orderNo); _unitOfWorkManage.CommitTran(); return WebResponseContent.Instance.OK($"取消分拣成功,恢复数量:{pickingRecord.PickQuantity}"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); _logger.LogError($"CancelPicking失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {barcode}, Error: {ex.Message}"); return WebResponseContent.Instance.Error($"取消分拣失败:{ex.Message}"); } } /// /// 回库 /// /// /// /// /// public async Task ReturnRemaining(string orderNo, string palletCode, string reason) { try { _unitOfWorkManage.BeginTran(); if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode)) return WebResponseContent.Instance.Error("订单号和托盘码不能为空"); // 获取库存和任务信息 var stockInfo = await _stockInfoService.Db.Queryable().FirstAsync(x => x.PalletCode == palletCode); if (stockInfo == null) return WebResponseContent.Instance.Error($"未找到托盘 {palletCode} 对应的库存信息"); var task = await GetCurrentTask(orderNo, palletCode); if (task == null) return WebResponseContent.Instance.Error("未找到对应的任务信息"); //分析需要回库的货物 //var returnAnalysis = await AnalyzeReturnItems(orderNo, palletCode, stockInfo.Id); //if (!returnAnalysis.HasItemsToReturn) // return await HandleNoReturnItems(orderNo, palletCode, task); var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id); if (!statusAnalysis.HasItemsToReturn) return await HandleNoReturnItems(orderNo, palletCode, task, stockInfo.Id); // 4. 检查是否有进行中的任务 if (statusAnalysis.HasActiveTasks) { return WebResponseContent.Instance.Error($"托盘 {palletCode} 有进行中的任务,不能执行回库操作"); } //执行回库操作 await ExecuteReturnOperations(orderNo, palletCode, stockInfo, task, statusAnalysis); _unitOfWorkManage.CommitTran(); // 创建回库任务 await CreateReturnTaskAndHandleESS(orderNo, palletCode, task, TaskTypeEnum.InPick); // 更新订单状态(不触发MES回传) await UpdateOrderStatusForReturn(orderNo); return WebResponseContent.Instance.OK($"回库操作成功,共回库数量:{statusAnalysis.TotalReturnQty}"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); _logger.LogError($"ReturnRemaining失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}"); return WebResponseContent.Instance.Error($"回库操作失败: {ex.Message}"); } } /// /// 空托盘取走接口(带订单号) /// 验证托盘是否真的为空,清理数据,更新订单状态,创建取托盘任务 /// public async Task RemoveEmptyPallet(string orderNo, string palletCode) { try { _unitOfWorkManage.BeginTran(); if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode)) return WebResponseContent.Instance.Error("订单号和托盘码不能为空"); // 检查订单是否存在 var order = await _outboundOrderService.Db.Queryable() .Where(x => x.OrderNo == orderNo) .FirstAsync(); if (order == null) return WebResponseContent.Instance.Error($"未找到订单 {orderNo}"); //检查托盘是否存在且属于该订单 var stockInfo = await _stockInfoService.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (stockInfo == null) return WebResponseContent.Instance.Error($"未找到托盘 {palletCode} 对应的库存信息"); var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id); if (!statusAnalysis.CanRemove) { if (!statusAnalysis.IsEmptyPallet) { return WebResponseContent.Instance.Error($"托盘 {palletCode} 上还有货物,不能取走"); } if (statusAnalysis.HasActiveTasks) { return WebResponseContent.Instance.Error($"托盘 {palletCode} 还有进行中的任务,不能取走"); } } // 清理零库存数据 await CleanupZeroStockData(stockInfo.Id); // 删除或取消相关任务 await HandleTaskCleanup(orderNo, palletCode); // 更新订单相关数据 await UpdateOrderData(orderNo, palletCode); _unitOfWorkManage.CommitTran(); _logger.LogInformation($"空托盘取走操作成功 - 订单: {orderNo}, 托盘: {palletCode}, 操作人: {App.User.UserName}"); return WebResponseContent.Instance.OK("空托盘取走操作成功"); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); _logger.LogError($"RemoveEmptyPallet失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}"); return WebResponseContent.Instance.Error($"空托盘取走失败: {ex.Message}"); } } #endregion #region 分拣确认私有方法 private async Task> ValidatePickingRequest(string orderNo, string palletCode, string barcode) { // 1. 基础参数验证 if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode) || string.IsNullOrEmpty(barcode)) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error("订单号、托盘码和条码不能为空"); // 2. 查找有效的锁定信息 var lockInfo = await FindValidLockInfo(orderNo, palletCode, barcode); if (lockInfo == null) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"未找到有效的锁定信息"); // 3. 检查订单状态 var order = await _outboundOrderService.Db.Queryable() .Where(x => x.OrderNo == orderNo) .FirstAsync(); if (order?.OrderStatus == (int)OutOrderStatusEnum.出库完成) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"订单{orderNo}已完成,不能继续分拣"); // 4. 获取订单明细 var orderDetail = await _outboundOrderDetailService.Db.Queryable() .FirstAsync(x => x.Id == lockInfo.OrderDetailId); if (orderDetail == null) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"未找到订单明细"); // 5. 检查订单明细数量 if (orderDetail.OverOutQuantity >= orderDetail.NeedOutQuantity) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"订单明细需求数量已满足"); // 6. 获取库存明细 var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(x => x.Barcode == barcode && x.StockId == lockInfo.StockId && x.Status != StockStatusEmun.入库确认.ObjToInt()) .FirstAsync(); if (stockDetail == null) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"无效的条码或物料编码"); // 7. 检查库存状态和数量 if (stockDetail.StockQuantity <= 0) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"条码{barcode}库存不足"); if (stockDetail.Status != StockStatusEmun.出库锁定.ObjToInt()) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"条码{barcode}状态不正确,无法分拣"); // 8. 检查是否重复分拣 var existingPicking = await Db.Queryable() .Where(x => x.Barcode == barcode && x.OrderNo == orderNo && x.PalletCode == palletCode && x.OutStockLockId == lockInfo.Id) .FirstAsync(); if (existingPicking != null) return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Error($"条码{barcode}已经分拣过"); return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail)>.Success((lockInfo, orderDetail, stockDetail)); } private async Task FindValidLockInfo(string orderNo, string palletCode, string barcode) { // 优先查找精确匹配的记录 var lockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.Status == (int)OutLockStockStatusEnum.出库中 && it.PalletCode == palletCode && it.CurrentBarcode == barcode && it.AssignQuantity > it.PickedQty).FirstAsync(); if (lockInfo == null) { // 查找同一订单下的记录 lockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.CurrentBarcode == barcode && it.Status == (int)OutLockStockStatusEnum.出库中 && it.AssignQuantity > it.PickedQty).FirstAsync(); if (lockInfo == null) { // 检查是否已经完成分拣 var completedLockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.CurrentBarcode == barcode && (it.Status == (int)OutLockStockStatusEnum.拣选完成 || it.PickedQty >= it.AssignQuantity)).FirstAsync(); if (completedLockInfo != null) throw new Exception($"条码{barcode}已经完成分拣,不能重复分拣"); else return null; } } return lockInfo; } private async Task> CalculateActualPickingQuantity( Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail, Dt_StockInfoDetail stockDetail) { decimal plannedQty = lockInfo.AssignQuantity - lockInfo.PickedQty; decimal remainingOrderQty = orderDetail.NeedOutQuantity - orderDetail.OverOutQuantity; decimal stockQuantity = stockDetail.StockQuantity; if (plannedQty <= 0) { return ValidationResult<(decimal, string)>.Error($"计划拣选数量必须大于0,当前: {plannedQty}"); } if (remainingOrderQty <= 0) { return ValidationResult<(decimal, string)>.Error($"订单剩余需求数量必须大于0,当前: {remainingOrderQty}"); } if (stockQuantity <= 0) { return ValidationResult<(decimal, string)>.Error($"库存数量必须大于0,当前: {stockQuantity}"); } // 三重检查:取最小值 decimal actualQty = plannedQty; string adjustedReason = null; if (plannedQty > remainingOrderQty) { actualQty = remainingOrderQty; adjustedReason = $"订单数量限制:从{plannedQty}调整为{actualQty}"; } if (actualQty > stockQuantity) { actualQty = stockQuantity; adjustedReason = adjustedReason != null ? $"{adjustedReason},库存数量限制:进一步调整为{actualQty}" : $"库存数量限制:从{plannedQty}调整为{actualQty}"; } if (actualQty <= 0) { return ValidationResult<(decimal, string)>.Error($"无法分拣:计算后的实际数量为{actualQty}"); } decimal projectedOverOut = orderDetail.OverOutQuantity + actualQty; if (projectedOverOut > orderDetail.NeedOutQuantity) { // 如果会超拣,调整为刚好满足需求的数量 actualQty = orderDetail.NeedOutQuantity - orderDetail.OverOutQuantity; adjustedReason = adjustedReason != null ? $"{adjustedReason},防超拣限制:最终调整为{actualQty}" : $"防超拣限制:从{plannedQty}调整为{actualQty}"; } if (adjustedReason != null) { _logger.LogWarning($"分拣数量调整:{adjustedReason},订单{orderDetail.NeedOutQuantity},已出库{orderDetail.OverOutQuantity},库存{stockQuantity}"); } return ValidationResult<(decimal, string)>.Success((actualQty, adjustedReason)); } /// /// 专门验证是否会发生超拣 /// private async Task> ValidateOverPicking(int orderDetailId, decimal pickingQty) { var orderDetail = await _outboundOrderDetailService.Db.Queryable() .FirstAsync(x => x.Id == orderDetailId); if (orderDetail == null) return ValidationResult.Error("未找到订单明细"); decimal projectedOverOut = orderDetail.OverOutQuantity + pickingQty; if (projectedOverOut > orderDetail.NeedOutQuantity) { return ValidationResult.Error( $"分拣后将导致超拣:当前已出库{orderDetail.OverOutQuantity},本次分拣{pickingQty},合计{projectedOverOut},超过需求{orderDetail.NeedOutQuantity}"); } return ValidationResult.Success(true); } private async Task ExecutePickingLogic( Dt_OutStockLockInfo lockInfo, Dt_OutboundOrderDetail orderDetail, Dt_StockInfoDetail stockDetail, string orderNo, string palletCode, string barcode, decimal actualQty) { decimal stockQuantity = stockDetail.StockQuantity; var result = new PickingResult { FinalLockInfo = lockInfo, FinalBarcode = barcode, FinalStockId = stockDetail.Id, ActualPickedQty = actualQty }; decimal finalPickedQty = actualQty; if (actualQty < stockQuantity) { // 拆包场景 await HandleSplitPacking(lockInfo, stockDetail, actualQty, stockQuantity, result); finalPickedQty = actualQty; } else if (actualQty == stockQuantity) { // 整包拣选 await HandleFullPicking(lockInfo, stockDetail, actualQty, result); finalPickedQty = actualQty; } else { // 部分拣选(库存不足) await HandlePartialPicking(lockInfo, stockDetail, actualQty, stockQuantity, result); finalPickedQty = result.ActualPickedQty; // 可能被调整 } // 统一更新订单数据(所有分支都从这里更新) await UpdateOrderRelatedData(lockInfo.OrderDetailId, finalPickedQty, orderNo); return result; } private async Task HandleSplitPacking(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, decimal actualQty, decimal stockQuantity, PickingResult result) { decimal remainingStockQty = stockQuantity - actualQty; // 更新原条码库存 stockDetail.StockQuantity = remainingStockQty; stockDetail.OutboundQuantity = remainingStockQty; await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); //生成新条码 string newBarcode = await GenerateNewBarcode(); //创建新锁定信息 var newLockInfo = await CreateSplitLockInfo(lockInfo, actualQty, newBarcode); // 记录拆包历史 await RecordSplitHistory(lockInfo, stockDetail, actualQty, remainingStockQty, newBarcode); // 更新原锁定信息 lockInfo.AssignQuantity = remainingStockQty; lockInfo.PickedQty = 0; lockInfo.Operator = App.User.UserName; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); // 设置结果 result.FinalLockInfo = newLockInfo; result.FinalBarcode = newBarcode; result.SplitResults.AddRange(CreateSplitResults(lockInfo, actualQty, remainingStockQty, newBarcode, stockDetail.Barcode)); _logger.LogInformation($"拆包分拣更新订单明细 - OrderDetailId: {lockInfo.OrderDetailId}, 分拣数量: {actualQty}"); } private async Task HandleFullPicking(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, decimal actualQty, PickingResult result) { // 1. 更新库存 stockDetail.StockQuantity = 0; stockDetail.OutboundQuantity = 0; stockDetail.Status = StockStatusEmun.出库完成.ObjToInt(); await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); // 2. 更新锁定信息 lockInfo.PickedQty += actualQty; lockInfo.Status = (int)OutLockStockStatusEnum.拣选完成; lockInfo.Operator = App.User.UserName; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); } private async Task HandlePartialPicking(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, decimal actualQty, decimal stockQuantity, PickingResult result) { decimal stockOutQty = stockQuantity; decimal remainingAssignQty = actualQty - stockQuantity; // 1. 更新库存 stockDetail.StockQuantity = 0; stockDetail.OutboundQuantity = 0; stockDetail.Status = StockStatusEmun.出库完成.ObjToInt(); await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); // 2. 更新锁定信息 lockInfo.PickedQty += stockOutQty; lockInfo.AssignQuantity = remainingAssignQty; lockInfo.Operator = App.User.UserName; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); // 3. 更新拆包记录状态 await UpdateSplitRecordsStatus(stockDetail.Barcode); result.ActualPickedQty = stockOutQty; } private async Task UpdateOrderRelatedData(int orderDetailId, decimal pickedQty, string orderNo) { // 获取最新的订单明细数据 var currentOrderDetail = await _outboundOrderDetailService.Db.Queryable() .FirstAsync(x => x.Id == orderDetailId); decimal newOverOutQuantity = currentOrderDetail.OverOutQuantity + pickedQty; decimal newPickedQty = currentOrderDetail.PickedQty + pickedQty; if (newOverOutQuantity > currentOrderDetail.NeedOutQuantity) { _logger.LogError($"防超拣检查失败 - OrderDetailId: {orderDetailId}, 已出库: {newOverOutQuantity}, 需求: {currentOrderDetail.NeedOutQuantity}, 本次分拣: {pickedQty}"); decimal adjustedQty = currentOrderDetail.NeedOutQuantity - currentOrderDetail.OverOutQuantity; if (adjustedQty > 0) { _logger.LogWarning($"自动调整分拣数量防止超拣:从{pickedQty}调整为{adjustedQty}"); newOverOutQuantity = currentOrderDetail.NeedOutQuantity; newPickedQty = currentOrderDetail.PickedQty + adjustedQty; } else { throw new Exception($"分拣后将导致已出库数量({newOverOutQuantity})超过订单需求数量({currentOrderDetail.NeedOutQuantity}),且无法自动调整"); } } // 更新订单明细 await _outboundOrderDetailService.Db.Updateable() .SetColumns(it => new Dt_OutboundOrderDetail { PickedQty = newPickedQty, OverOutQuantity = newOverOutQuantity, }) .Where(it => it.Id == orderDetailId) .ExecuteCommandAsync(); // 检查并更新订单状态 await CheckAndUpdateOrderStatus(orderNo); } private async Task RecordPickingHistory(PickingResult result, string orderNo, string palletCode) { var task = await _taskRepository.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode) .FirstAsync(); if (result.FinalLockInfo.Id <= 0) { throw new Exception($"锁定信息ID无效: {result.FinalLockInfo.Id},无法记录拣选历史"); } var pickingHistory = new Dt_PickingRecord { FactoryArea = result.FinalLockInfo.FactoryArea, TaskNo = task?.TaskNum ?? 0, LocationCode = task?.SourceAddress ?? "", StockId = result.FinalStockId, OrderNo = orderNo, OrderDetailId = result.FinalLockInfo.OrderDetailId, PalletCode = palletCode, Barcode = result.FinalBarcode, MaterielCode = result.FinalLockInfo.MaterielCode, PickQuantity = result.ActualPickedQty, PickTime = DateTime.Now, Operator = App.User.UserName, OutStockLockId = result.FinalLockInfo.Id }; await Db.Insertable(pickingHistory).ExecuteCommandAsync(); } #endregion #region 取消分拣私有方法 private async Task> ValidateCancelRequest(string orderNo, string palletCode, string barcode) { // 基础参数验证 if (string.IsNullOrEmpty(orderNo) || string.IsNullOrEmpty(palletCode) || string.IsNullOrEmpty(barcode)) return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error("订单号、托盘码和条码不能为空"); // 查找拣选记录 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 ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error("未找到对应的拣选记录"); if (pickingRecord.PickQuantity <= 0) { return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error($"拣选记录数量无效: {pickingRecord.PickQuantity}"); } // 查找锁定信息 var lockInfo = await _outStockLockInfoService.Db.Queryable() .Where(it => it.Id == pickingRecord.OutStockLockId) .FirstAsync(); if (lockInfo == null) return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error("未找到对应的出库锁定信息"); if (lockInfo.PickedQty < pickingRecord.PickQuantity) { return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error( $"取消数量({pickingRecord.PickQuantity})超过锁定信息的已拣选数量({lockInfo.PickedQty})"); } // 检查状态是否允许取消 if (lockInfo.Status != (int)OutLockStockStatusEnum.拣选完成) return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error("当前状态不允许取消分拣"); var order = await _outboundOrderService.Db.Queryable().FirstAsync(x => x.OrderNo == orderNo); if (order?.OrderStatus == (int)OutOrderStatusEnum.出库完成) return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error("订单已出库完成,不允许取消分拣"); var orderDetail = await _outboundOrderDetailService.Db.Queryable().FirstAsync(x => x.Id == pickingRecord.OrderDetailId); if (orderDetail == null) return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error($"未找到订单明细,ID: {pickingRecord.OrderDetailId}"); // 检查订单明细的已拣选数量是否足够取消 if (orderDetail.PickedQty < pickingRecord.PickQuantity) { return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error($"取消数量({pickingRecord.PickQuantity})超过订单明细的已拣选数量({orderDetail.PickedQty})"); } // 检查订单明细的已出库数量是否足够取消 if (orderDetail.OverOutQuantity < pickingRecord.PickQuantity) { return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error($"取消数量({pickingRecord.PickQuantity})超过订单明细的已出库数量({orderDetail.OverOutQuantity})"); } var stockDetail = await _stockInfoDetailService.Db.Queryable().FirstAsync(it => it.Barcode == barcode && it.StockId == pickingRecord.StockId); if (stockDetail != null) { // 检查库存状态 - 如果状态是入库确认或入库完成,说明已经回库 if (stockDetail.Status == StockStatusEmun.入库确认.ObjToInt() || stockDetail.Status == StockStatusEmun.入库完成.ObjToInt()) { return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Error($"条码{barcode}已经回库,不能取消分拣"); } } return ValidationResult<(Dt_PickingRecord, Dt_OutStockLockInfo, Dt_OutboundOrderDetail)>.Success((pickingRecord, lockInfo, orderDetail)); } /// /// 检查条码是否已经回库 /// private async Task IsBarcodeReturned(string barcode, int stockId) { var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == barcode && it.StockId == stockId) .FirstAsync(); if (stockDetail == null) return false; // 如果状态是入库确认或入库完成,说明已经回库 return stockDetail.Status == StockStatusEmun.入库确认.ObjToInt() || stockDetail.Status == StockStatusEmun.入库完成.ObjToInt(); } /// /// 检查锁定信息对应的条码是否已经回库 /// private async Task IsLockInfoReturned(Dt_OutStockLockInfo lockInfo) { var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == lockInfo.CurrentBarcode && it.StockId == lockInfo.StockId) .FirstAsync(); if (stockDetail == null) return false; return stockDetail.Status == StockStatusEmun.入库确认.ObjToInt() || stockDetail.Status == StockStatusEmun.入库完成.ObjToInt(); } private async Task ExecuteCancelLogic(Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord, Dt_OutboundOrderDetail orderDetail, string orderNo) { decimal cancelQty = pickingRecord.PickQuantity; var currentStockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == pickingRecord.Barcode && it.StockId == pickingRecord.StockId) .FirstAsync(); if (currentStockDetail != null && (currentStockDetail.Status == StockStatusEmun.入库确认.ObjToInt() || currentStockDetail.Status == StockStatusEmun.入库完成.ObjToInt())) { throw new Exception($"条码{pickingRecord.Barcode}已经回库,无法取消分拣"); } // 检查取消后数量不会为负数 decimal newOverOutQuantity = orderDetail.OverOutQuantity - cancelQty; decimal newPickedQty = orderDetail.PickedQty - cancelQty; if (newOverOutQuantity < 0 || newPickedQty < 0) { throw new Exception($"取消分拣将导致数据异常:已出库{newOverOutQuantity},已拣选{newPickedQty}"); } // 处理不同类型的取消 if (lockInfo.IsSplitted == 1 && lockInfo.ParentLockId.HasValue) { await HandleSplitBarcodeCancel(lockInfo, pickingRecord, cancelQty); } else { await HandleNormalBarcodeCancel(lockInfo, pickingRecord, cancelQty); } // 更新订单明细 await UpdateOrderDetailOnCancel(pickingRecord.OrderDetailId, cancelQty); // 删除拣选记录 await Db.Deleteable() .Where(x => x.Id == pickingRecord.Id) .ExecuteCommandAsync(); // 重新检查订单状态 await UpdateOrderStatusForReturn(orderNo); } private async Task HandleSplitBarcodeCancel(Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord, decimal cancelQty) { // 查找父锁定信息 var parentLockInfo = await _outStockLockInfoService.Db.Queryable() .Where(x => x.Id == lockInfo.ParentLockId.Value) .FirstAsync(); if (parentLockInfo == null) throw new Exception("未找到父锁定信息,无法取消拆包分拣"); if (await IsLockInfoReturned(parentLockInfo)) { throw new Exception($"父条码{parentLockInfo.CurrentBarcode}已经回库,无法取消拆包分拣"); } if (await IsLockInfoReturned(lockInfo)) { throw new Exception($"拆包条码{lockInfo.CurrentBarcode}已经回库,无法取消拆包分拣"); } // 恢复父锁定信息的分配数量 parentLockInfo.AssignQuantity += cancelQty; await _outStockLockInfoService.Db.Updateable(parentLockInfo).ExecuteCommandAsync(); // 恢复库存 var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(x => x.Barcode == parentLockInfo.CurrentBarcode && x.StockId == parentLockInfo.StockId) .FirstAsync(); if (stockDetail != null) { stockDetail.StockQuantity += cancelQty; stockDetail.OutboundQuantity = stockDetail.StockQuantity; stockDetail.Status = StockStatusEmun.出库锁定.ObjToInt(); await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); } // 更新拆包记录状态 await _splitPackageService.Db.Updateable() .SetColumns(x => new Dt_SplitPackageRecord { Status = (int)SplitPackageStatusEnum.已撤销, IsReverted = true, }) .Where(x => x.NewBarcode == lockInfo.CurrentBarcode && !x.IsReverted) .ExecuteCommandAsync(); // 删除拆包产生的锁定信息 await _outStockLockInfoService.Db.Deleteable() .Where(x => x.Id == lockInfo.Id) .ExecuteCommandAsync(); await UpdateOrderDetailOnCancel(pickingRecord.OrderDetailId, cancelQty); } private async Task HandleNormalBarcodeCancel(Dt_OutStockLockInfo lockInfo, Dt_PickingRecord pickingRecord, decimal cancelQty) { if (await IsLockInfoReturned(lockInfo)) { throw new Exception($"条码{lockInfo.CurrentBarcode}已经回库,无法取消分拣"); } // 恢复锁定信息 lockInfo.PickedQty -= cancelQty; if (lockInfo.PickedQty < 0) lockInfo.PickedQty = 0; lockInfo.Status = (int)OutLockStockStatusEnum.出库中; await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync(); // 恢复库存 var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(x => x.Barcode == pickingRecord.Barcode && x.StockId == pickingRecord.StockId) .FirstAsync(); if (stockDetail != null) { stockDetail.StockQuantity += cancelQty; stockDetail.OutboundQuantity = stockDetail.StockQuantity; stockDetail.Status = StockStatusEmun.出库锁定.ObjToInt(); await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync(); } } private async Task UpdateOrderDetailOnCancel(int orderDetailId, decimal cancelQty) { // 获取最新的订单明细数据 var currentOrderDetail = await _outboundOrderDetailService.Db.Queryable() .FirstAsync(x => x.Id == orderDetailId); decimal newOverOutQuantity = currentOrderDetail.OverOutQuantity - cancelQty; decimal newPickedQty = currentOrderDetail.PickedQty - cancelQty; // 检查取消后数量不会为负数 if (newOverOutQuantity < 0 || newPickedQty < 0) { throw new Exception($"取消分拣将导致已出库数量({newOverOutQuantity})或已拣选数量({newPickedQty})为负数"); } await _outboundOrderDetailService.Db.Updateable() .SetColumns(it => new Dt_OutboundOrderDetail { PickedQty = newPickedQty, OverOutQuantity = newOverOutQuantity, }) .Where(it => it.Id == orderDetailId) .ExecuteCommandAsync(); } #endregion #region 回库操作私有方法 private async Task GetStockInfo(string palletCode) { return await _stockInfoService.Db.Queryable() .FirstAsync(x => x.PalletCode == palletCode); } /// /// 检查整个托盘是否已经回库 /// private async Task IsPalletReturned(string palletCode) { var stockInfo = await _stockInfoService.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (stockInfo == null) return false; // 如果托盘状态是入库确认或入库完成,说明已经回库 return stockInfo.StockStatus == StockStatusEmun.入库确认.ObjToInt() || stockInfo.StockStatus == StockStatusEmun.入库完成.ObjToInt(); } private async Task GetCurrentTask(string orderNo, string palletCode) { // 先尝试通过订单号和托盘号查找任务 var task = await _taskRepository.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode) .FirstAsync(); if (task == null) { // 如果找不到,再通过托盘号查找 task = await _taskRepository.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); } return task; } private async Task CalculateSplitReturnQuantity(List splitRecords, int stockId) { decimal totalQty = 0; var processedBarcodes = new HashSet(); foreach (var splitRecord in splitRecords) { // 检查原条码 if (!processedBarcodes.Contains(splitRecord.OriginalBarcode)) { var originalStock = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == splitRecord.OriginalBarcode && it.StockId == stockId) .FirstAsync(); if (originalStock != null && originalStock.StockQuantity > 0) { totalQty += originalStock.StockQuantity; processedBarcodes.Add(splitRecord.OriginalBarcode); } } // 检查新条码 if (!processedBarcodes.Contains(splitRecord.NewBarcode)) { var newStock = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == splitRecord.NewBarcode && it.StockId == stockId) .FirstAsync(); if (newStock != null && newStock.StockQuantity > 0) { totalQty += newStock.StockQuantity; processedBarcodes.Add(splitRecord.NewBarcode); } } } return totalQty; } private async Task HandleNoReturnItems(string orderNo, string palletCode, Dt_Task originalTask, int stockInfoId) { // 检查是否所有货物都已拣选完成 //var allPicked = await _outStockLockInfoService.Db.Queryable() // .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode) // .AnyAsync(it => it.Status == (int)OutLockStockStatusEnum.拣选完成); //if (allPicked) //{ // // 删除原始出库任务 组空盘 空盘回库 // //await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync(); // return WebResponseContent.Instance.OK("所有货物已拣选完成,托盘为空"); //} //else //{ // // 删除原始出库任务 // //await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync(); // return WebResponseContent.Instance.Error("没有需要回库的剩余货物"); //} try { var locationtype = 0; var stockInfo = await _stockInfoService.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (stockInfo == null) { var firstLocation = await _locationInfoService.Db.Queryable().FirstAsync(x => x.LocationCode == originalTask.SourceAddress); locationtype = firstLocation?.LocationType ?? 1; } else { locationtype = stockInfo.LocationType; } var targetAddress = originalTask.TargetAddress; await CleanupZeroStockData(stockInfoId); var emptystockInfo = new Dt_StockInfo() { PalletType = PalletTypeEnum.Empty.ObjToInt(), StockStatus = StockStatusEmun.组盘暂存.ObjToInt(), PalletCode = palletCode, LocationType = locationtype }; emptystockInfo.Details = new List(); _stockInfoService.AddMaterielGroup(emptystockInfo); //空托盘如何处理 还有一个出库任务要处理。 originalTask.PalletType = PalletTypeEnum.Empty.ObjToInt(); await CreateReturnTaskAndHandleESS(orderNo, palletCode, originalTask, TaskTypeEnum.InEmpty); } catch (Exception ex) { _logger.LogError($" HandleNoReturnItems 失败: {ex.Message}"); return WebResponseContent.Instance.Error($" 回库空托盘失败!"); } return WebResponseContent.Instance.OK("空托盘回库任务创建成功"); } private async Task ExecuteReturnOperations(string orderNo, string palletCode, Dt_StockInfo stockInfo, Dt_Task task, PalletStatusAnalysis analysis) { // 情况1:处理未分拣的出库锁定记录 if (analysis.HasRemainingLocks) { await HandleRemainingLocksReturn(analysis.RemainingLocks, stockInfo.Id); // await UpdateOrderDetailsOnReturn(analysis.RemainingLocks); } // 处理托盘上其他库存货物 if (analysis.HasPalletStockGoods) { await HandlePalletStockGoodsReturn(analysis.PalletStockGoods); } // 处理拆包记录 if (analysis.HasSplitRecords) { await HandleSplitRecordsReturn(analysis.SplitRecords, orderNo, palletCode); } // 更新库存主表状态 await UpdateStockInfoStatus(stockInfo); } private async Task HandleRemainingLocksReturn(List remainingLocks, int stockId) { var lockIds = remainingLocks.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 remainingLocks) { decimal returnQty = lockInfo.AssignQuantity - lockInfo.PickedQty; // 查找对应的库存明细 var stockDetail = await _stockInfoDetailService.Db.Queryable() .Where(it => it.Barcode == lockInfo.CurrentBarcode && it.StockId == lockInfo.StockId) .FirstAsync(); if (stockDetail != null) { // 恢复库存状态 stockDetail.OutboundQuantity = Math.Max(0, stockDetail.OutboundQuantity - returnQty); stockDetail.Status = StockStatusEmun.入库确认.ObjToInt(); await _stockInfoDetailService.Db.Updateable(stockDetail).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(); } } } private async Task UpdateOrderDetailsOnReturn(List remainingLocks) { // 按订单明细分组 var orderDetailGroups = remainingLocks.GroupBy(x => x.OrderDetailId); foreach (var group in orderDetailGroups) { var orderDetailId = group.Key; var totalReturnQty = group.Sum(x => x.AssignQuantity - x.PickedQty); // 获取当前订单明细 var orderDetail = await _outboundOrderDetailService.Db.Queryable() .FirstAsync(x => x.Id == orderDetailId); if (orderDetail != null) { // 调整已拣选数量和已出库数量 decimal newPickedQty = Math.Max(0, orderDetail.PickedQty - totalReturnQty); decimal newOverOutQuantity = Math.Max(0, orderDetail.OverOutQuantity - totalReturnQty); await _outboundOrderDetailService.Db.Updateable() .SetColumns(it => new Dt_OutboundOrderDetail { PickedQty = newPickedQty, OverOutQuantity = newOverOutQuantity, }) .Where(it => it.Id == orderDetailId) .ExecuteCommandAsync(); } } } private async Task HandlePalletStockGoodsReturn(List palletStockGoods) { _logger.LogInformation($"回库操作:发现{palletStockGoods.Count}个库存明细需要回库,等待AGV搬运"); foreach (var stockGood in palletStockGoods) { _logger.LogInformation($"待回库货物 - 条码: {stockGood.Barcode}, 数量: {stockGood.StockQuantity}, 当前状态: {stockGood.Status}"); // 恢复库存状态 stockGood.OutboundQuantity = 0; stockGood.Status = StockStatusEmun.入库确认.ObjToInt(); await _stockInfoDetailService.Db.Updateable(stockGood).ExecuteCommandAsync(); } } private async Task HandleSplitRecordsReturn(List splitRecords, string orderNo, string palletCode) { // 更新拆包记录状态 await _splitPackageService.Db.Updateable() .SetColumns(x => new Dt_SplitPackageRecord { Status = (int)SplitPackageStatusEnum.已回库, }) .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode && !x.IsReverted) .ExecuteCommandAsync(); } private async Task UpdateStockInfoStatus(Dt_StockInfo stockInfo) { _logger.LogInformation($"回库操作:托盘{stockInfo.PalletCode}等待AGV回库搬运"); // 更新库存主表状态 stockInfo.StockStatus = StockStatusEmun.入库确认.ObjToInt(); await _stockInfoService.Db.Updateable(stockInfo).ExecuteCommandAsync(); } /// /// 创建回库任务 /// /// /// /// /// /// private async Task CreateReturnTaskAndHandleESS(string orderNo, string palletCode, Dt_Task originalTask, TaskTypeEnum taskTypeEnum) { var firstLocation = await _locationInfoService.Db.Queryable() .FirstAsync(x => x.LocationCode == originalTask.SourceAddress); // 分配新货位 var newLocation = _locationInfoService.AssignLocation(firstLocation.LocationType); Dt_Task returnTask = 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.ObjToInt(), PalletType = originalTask.PalletType, WarehouseId = originalTask.WarehouseId }; // 保存回库任务 await _taskRepository.Db.Insertable(returnTask).ExecuteCommandAsync(); var targetAddress = originalTask.TargetAddress; // 删除原始出库任务 _taskRepository.DeleteAndMoveIntoHty(originalTask, OperateTypeEnum.自动完成); // await _taskRepository.Db.Deleteable(originalTask).ExecuteCommandAsync(); // 给 ESS 发送流动信号和创建任务 await SendESSCommands(palletCode, targetAddress, returnTask); } /// /// 给ESS下任务 /// /// /// /// /// /// private async Task SendESSCommands(string palletCode, string targetAddress, Dt_Task returnTask) { try { // 1. 发送流动信号 var moveResult = await _eSSApiService.MoveContainerAsync(new WIDESEA_DTO.Basic.MoveContainerRequest { slotCode = movestations[targetAddress], containerCode = palletCode }); if (moveResult) { // 2. 创建回库任务 var essTask = new TaskModel() { taskType = "putaway", taskGroupCode = "", groupPriority = 0, tasks = new List{ new() { taskCode = returnTask.TaskNum.ToString(), taskPriority = 0, taskDescribe = new TaskDescribeType { containerCode = palletCode, containerType = "CT_KUBOT_STANDARD", fromLocationCode = stations.GetValueOrDefault(targetAddress) ?? "", toStationCode = "", toLocationCode = returnTask.TargetAddress, deadline = 0, storageTag = "" } } } }; var resultTask = await _eSSApiService.CreateTaskAsync(essTask); _logger.LogInformation($"ReturnRemaining 创建任务成功: {resultTask}"); } } catch (Exception ex) { _logger.LogError($"ReturnRemaining ESS命令发送失败: {ex.Message}"); throw new Exception($"ESS系统通信失败: {ex.Message}"); } } #endregion #region 订单状态管理 private async Task CheckAndUpdateOrderStatus(string orderNo) { try { var orderDetails = await _outboundOrderDetailService.Db.Queryable() .LeftJoin((o, item) => o.OrderId == item.Id) .Where((o, item) => item.OrderNo == orderNo) .Select((o, item) => o) .ToListAsync(); bool allCompleted = true; foreach (var detail in orderDetails) { if (detail.OverOutQuantity < detail.NeedOutQuantity) { allCompleted = false; break; } } var outboundOrder = await _outboundOrderService.Db.Queryable() .FirstAsync(x => x.OrderNo == orderNo); if (outboundOrder == null) return; int newStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中; if (outboundOrder.OrderStatus != newStatus) { await _outboundOrderService.Db.Updateable() .SetColumns(x => x.OrderStatus == newStatus) .Where(x => x.OrderNo == orderNo) .ExecuteCommandAsync(); // 只有正常分拣完成时才向MES反馈 if (allCompleted && newStatus == (int)OutOrderStatusEnum.出库完成) { await HandleOrderCompletion(outboundOrder, orderNo); } } } catch (Exception ex) { _logger.LogError($"CheckAndUpdateOrderStatus失败 - OrderNo: {orderNo}, Error: {ex.Message}"); } } private async Task UpdateOrderStatusForReturn(string orderNo) { try { var orderDetails = await _outboundOrderDetailService.Db.Queryable() .LeftJoin((o, item) => o.OrderId == item.Id) .Where((o, item) => item.OrderNo == orderNo) .Select((o, item) => o) .ToListAsync(); bool allCompleted = true; foreach (var detail in orderDetails) { if (detail.OverOutQuantity < detail.NeedOutQuantity) { allCompleted = false; break; } } var outboundOrder = await _outboundOrderService.Db.Queryable() .FirstAsync(x => x.OrderNo == orderNo); if (outboundOrder == null) return; int newStatus = allCompleted ? (int)OutOrderStatusEnum.出库完成 : (int)OutOrderStatusEnum.出库中; if (outboundOrder.OrderStatus != newStatus) { await _outboundOrderService.Db.Updateable() .SetColumns(x => x.OrderStatus == newStatus) .Where(x => x.OrderNo == orderNo) .ExecuteCommandAsync(); _logger.LogInformation($"回库操作更新订单状态 - OrderNo: {orderNo}, 新状态: {newStatus}"); } } catch (Exception ex) { _logger.LogError($"UpdateOrderStatusForReturn失败 - OrderNo: {orderNo}, Error: {ex.Message}"); } } private async Task HandleOrderCompletion(Dt_OutboundOrder outboundOrder, string orderNo) { // 调拨出库和重检出库不需要反馈MES if (outboundOrder.OrderType == OutOrderTypeEnum.Allocate.ObjToInt() || outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt()) { return; } try { var feedmodel = new FeedbackOutboundRequestModel { reqCode = Guid.NewGuid().ToString(), reqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), business_type = outboundOrder.BusinessType, factoryArea = outboundOrder.FactoryArea, operationType = 1, Operator = App.User.UserName, orderNo = outboundOrder.UpperOrderNo, documentsNO = outboundOrder.OrderNo, status = outboundOrder.OrderStatus, details = new List() }; // 只获取已拣选完成的锁定记录 var lists = await _outStockLockInfoService.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.Status == (int)OutLockStockStatusEnum.拣选完成) .ToListAsync(); 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, qty = group.Sum(x => x.PickedQty), currentDeliveryQty = group.Sum(x => x.PickedQty), 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.PickedQty }).ToList() }).ToList(); feedmodel.details = groupedData; var result = await _invokeMESService.FeedbackOutbound(feedmodel); if (result != null && result.code == 200) { await _outboundOrderDetailService.Db.Updateable() .SetColumns(x => x.ReturnToMESStatus == 1) .Where(x => x.OrderId == outboundOrder.Id) .ExecuteCommandAsync(); await _outboundOrderService.Db.Updateable() .SetColumns(x => x.ReturnToMESStatus == 1) .Where(x => x.OrderNo == orderNo) .ExecuteCommandAsync(); } _logger.LogError($"FeedbackOutbound成功 - OrderNo: {orderNo}, {JsonSerializer.Serialize(result)}"); } catch (Exception ex) { _logger.LogError($"FeedbackOutbound失败 - OrderNo: {orderNo}, Error: {ex.Message}"); } } #endregion #region 空托盘 /// /// 清理零库存数据 /// private async Task CleanupZeroStockData(int stockId) { try { // 1. 删除库存数量为0的明细记录 var deleteDetailCount = await _stockInfoDetailService.Db.Deleteable() .Where(x => x.StockId == stockId && x.StockQuantity == 0 && (x.Status == StockStatusEmun.出库完成.ObjToInt() || x.Status == StockStatusEmun.入库完成.ObjToInt())) .ExecuteCommandAsync(); await _stockInfoService.Db.Deleteable() .Where(x => x.Id == stockId).ExecuteCommandAsync(); _logger.LogInformation($"清理零库存明细记录 - StockId: {stockId}, 删除记录数: {deleteDetailCount}"); } catch (Exception ex) { _logger.LogWarning($"清理零库存数据失败 - StockId: {stockId}, Error: {ex.Message}"); // 注意:清理失败不应该影响主流程 } } /// /// 处理任务清理(按订单和托盘) /// private async Task HandleTaskCleanup(string orderNo, string palletCode) { try { // 1. 查找所有与该订单和托盘相关的任务 var tasks = await _taskRepository.Db.Queryable().Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode).ToListAsync(); if (tasks.Any()) { foreach (var task in tasks) { task.TaskStatus = (int)TaskStatusEnum.Finish; } // await _taskRepository.Db.Updateable(tasks).ExecuteCommandAsync(); _taskRepository.DeleteAndMoveIntoHty(tasks, OperateTypeEnum.自动完成); _logger.LogInformation($"完成{tasks.Count}个托盘任务 - 订单: {orderNo}, 托盘: {palletCode}"); } } catch (Exception ex) { _logger.LogWarning($"处理任务清理失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}"); throw new Exception($"任务清理失败: {ex.Message}"); } } /// /// 更新订单相关数据 /// private async Task UpdateOrderData(string orderNo, string palletCode) { try { // 检查订单是否还有其他托盘在处理中 var otherActivePallets = await _outStockLockInfoService.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode != palletCode && (x.Status == (int)OutLockStockStatusEnum.出库中 || x.Status == (int)OutLockStockStatusEnum.回库中)) .AnyAsync(); var otherActiveTasks = await _taskRepository.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode != palletCode // && x.TaskStatus.In((int)TaskStatusEnum.待执行, (int)TaskStatusEnum.执行中) ) .AnyAsync(); // 如果没有其他托盘在处理,检查订单是否应该完成 if (!otherActivePallets && !otherActiveTasks) { await CheckAndUpdateOrderCompletion(orderNo); } else { _logger.LogInformation($"订单 {orderNo} 还有其他托盘在处理,不更新订单状态"); } // 3. 更新拣选记录状态(可选) await UpdatePickingRecordsStatus(orderNo, palletCode); } catch (Exception ex) { _logger.LogWarning($"更新订单数据失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}"); throw new Exception($"更新订单数据失败: {ex.Message}"); } } /// /// 检查并更新订单完成状态 /// private async Task CheckAndUpdateOrderCompletion(string orderNo) { var orderDetails = await _outboundOrderDetailService.Db.Queryable() .LeftJoin((o, item) => o.OrderId == item.Id) .Where((o, item) => item.OrderNo == orderNo) .Select((o, item) => o) .ToListAsync(); bool allCompleted = true; foreach (var detail in orderDetails) { if (detail.OverOutQuantity < detail.NeedOutQuantity) { allCompleted = false; break; } } var outboundOrder = await _outboundOrderService.Db.Queryable() .FirstAsync(x => x.OrderNo == orderNo); if (outboundOrder != null && allCompleted && outboundOrder.OrderStatus != (int)OutOrderStatusEnum.出库完成) { outboundOrder.OrderStatus = (int)OutOrderStatusEnum.出库完成; await _outboundOrderService.Db.Updateable(outboundOrder).ExecuteCommandAsync(); _logger.LogInformation($"订单 {orderNo} 已标记为出库完成"); // 向MES反馈订单完成(如果需要) await HandleOrderCompletion(outboundOrder, orderNo); } } /// /// 更新拣选记录状态 /// private async Task UpdatePickingRecordsStatus(string orderNo, string palletCode) { try { // 可以将相关的拣选记录标记为已完成 var pickingRecords = await Db.Queryable() .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode) .ToListAsync(); // 这里可以根据需要更新拣选记录的状态字段 // 例如:pickingRecord.Status = (int)PickingStatusEnum.已完成; _logger.LogInformation($"找到{pickingRecords.Count}条拣选记录 - 订单: {orderNo}, 托盘: {palletCode}"); } catch (Exception ex) { _logger.LogWarning($"更新拣选记录状态失败: {ex.Message}"); } } #endregion #region 辅助方法 /// /// 统一分析托盘状态 - 返回托盘的完整状态信息 /// private async Task AnalyzePalletStatus(string orderNo, string palletCode, int stockId) { var result = new PalletStatusAnalysis { OrderNo = orderNo, PalletCode = palletCode, StockId = stockId }; // 1. 分析未分拣的出库锁定记录 var remainingLocks = await _outStockLockInfoService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && it.Status == (int)OutLockStockStatusEnum.出库中) .ToListAsync(); if (remainingLocks.Any()) { result.HasRemainingLocks = true; result.RemainingLocks = remainingLocks; result.RemainingLocksReturnQty = remainingLocks.Sum(x => x.AssignQuantity - x.PickedQty); } // 2. 分析托盘上的库存货物 var palletStockGoods = await _stockInfoDetailService.Db.Queryable() .Where(it => it.StockId == stockId && (it.Status == StockStatusEmun.入库确认.ObjToInt() || it.Status == StockStatusEmun.入库完成.ObjToInt() || it.Status == StockStatusEmun.出库锁定.ObjToInt())) .Where(it => it.StockQuantity > 0) .ToListAsync(); if (palletStockGoods.Any()) { result.HasPalletStockGoods = true; result.PalletStockGoods = palletStockGoods; result.PalletStockReturnQty = palletStockGoods.Sum(x => x.StockQuantity); } // 3. 分析拆包记录 var splitRecords = await _splitPackageService.Db.Queryable() .Where(it => it.OrderNo == orderNo && it.PalletCode == palletCode && !it.IsReverted && it.Status != (int)SplitPackageStatusEnum.已回库) .ToListAsync(); if (splitRecords.Any()) { result.HasSplitRecords = true; result.SplitRecords = splitRecords; result.SplitReturnQty = await CalculateSplitReturnQuantity(splitRecords, stockId); } // 4. 计算总回库数量和空托盘状态 result.TotalReturnQty = result.RemainingLocksReturnQty + result.PalletStockReturnQty + result.SplitReturnQty; result.HasItemsToReturn = result.TotalReturnQty > 0; result.IsEmptyPallet = !result.HasItemsToReturn; // 5. 检查是否有进行中的任务 result.HasActiveTasks = await _taskRepository.Db.Queryable() .Where(x => x.OrderNo == orderNo && x.TaskType == TaskTypeEnum.InPick.ObjToInt() && x.PalletCode == palletCode && x.TaskStatus == (int)TaskStatusEnum.New) .AnyAsync(); return result; } /// /// 检查托盘是否为空 /// private async Task IsPalletEmpty(string orderNo, string palletCode) { try { // 获取库存信息 var stockInfo = await _stockInfoService.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (stockInfo == null) return false; // 使用统一的状态分析 var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id); return statusAnalysis.IsEmptyPallet; } catch (Exception ex) { _logger.LogWarning($"检查托盘是否为空失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}"); return false; } } /// /// 检查并处理空托盘 /// private async Task CheckAndHandleEmptyPallet(string orderNo, string palletCode) { try { // 1. 获取库存信息 var stockInfo = await _stockInfoService.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (stockInfo == null) { _logger.LogWarning($"未找到托盘 {palletCode} 的库存信息"); return false; } // 2. 使用统一的状态分析 var statusAnalysis = await AnalyzePalletStatus(orderNo, palletCode, stockInfo.Id); // 3. 检查是否为空托盘且没有进行中的任务 if (!statusAnalysis.IsEmptyPallet || statusAnalysis.HasActiveTasks) { return false; } _logger.LogInformation($"检测到空托盘,开始自动处理 - 订单: {orderNo}, 托盘: {palletCode}"); //// 清理零库存数据 //await CleanupZeroStockData(stockInfo.Id); //// 更新库存主表状态为空托盘 //await UpdateStockInfoAsEmpty(stockInfo); //// 处理出库锁定记录 //await HandleOutStockLockRecords(orderNo, palletCode); //// 处理任务状态 //await HandleTaskStatusForEmptyPallet(orderNo, palletCode); //// 更新订单数据 //await UpdateOrderDataForEmptyPallet(orderNo, palletCode); ////记录操作历史 //await RecordAutoEmptyPalletOperation(orderNo, palletCode); _logger.LogInformation($"空托盘自动处理完成 - 订单: {orderNo}, 托盘: {palletCode}"); return true; } catch (Exception ex) { _logger.LogError($"自动处理空托盘失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}"); return false; } } private async Task GenerateNewBarcode() { var seq = await _dailySequenceService.GetNextSequenceAsync(); return "WSLOT" + DateTime.Now.ToString("yyyyMMdd") + seq.ToString()?.PadLeft(5, '0'); } private async Task CreateSplitLockInfo(Dt_OutStockLockInfo originalLock, decimal quantity, string newBarcode) { var newLockInfo = new Dt_OutStockLockInfo { OrderNo = originalLock.OrderNo, OrderDetailId = originalLock.OrderDetailId, BatchNo = originalLock.BatchNo, MaterielCode = originalLock.MaterielCode, MaterielName = originalLock.MaterielName, StockId = originalLock.StockId, OrderQuantity = quantity, OriginalQuantity = quantity, AssignQuantity = quantity, PickedQty = quantity, LocationCode = originalLock.LocationCode, PalletCode = originalLock.PalletCode, TaskNum = originalLock.TaskNum, Status = (int)OutLockStockStatusEnum.拣选完成, Unit = originalLock.Unit, SupplyCode = originalLock.SupplyCode, OrderType = originalLock.OrderType, CurrentBarcode = newBarcode, OriginalLockQuantity = quantity, IsSplitted = 1, ParentLockId = originalLock.Id, Operator = App.User.UserName, FactoryArea = originalLock.FactoryArea, lineNo = originalLock.lineNo, WarehouseCode = originalLock.WarehouseCode, }; var newLockId = await _outStockLockInfoService.Db.Insertable(newLockInfo).ExecuteReturnIdentityAsync(); newLockInfo.Id = newLockId; return newLockInfo; } private async Task RecordSplitHistory(Dt_OutStockLockInfo lockInfo, Dt_StockInfoDetail stockDetail, decimal splitQty, decimal remainQty, string newBarcode) { var splitHistory = new Dt_SplitPackageRecord { FactoryArea = lockInfo.FactoryArea, TaskNum = lockInfo.TaskNum, OutStockLockInfoId = lockInfo.Id, StockId = stockDetail.StockId, Operator = App.User.UserName, IsReverted = false, OriginalBarcode = stockDetail.Barcode, NewBarcode = newBarcode, SplitQty = splitQty, RemainQuantity = remainQty, MaterielCode = lockInfo.MaterielCode, SplitTime = DateTime.Now, OrderNo = lockInfo.OrderNo, PalletCode = lockInfo.PalletCode, Status = (int)SplitPackageStatusEnum.已拣选 }; await _splitPackageService.Db.Insertable(splitHistory).ExecuteCommandAsync(); } private List CreateSplitResults(Dt_OutStockLockInfo lockInfo, decimal splitQty, decimal remainQty, string newBarcode, string originalBarcode) { return new List { new SplitResult { materialCode = lockInfo.MaterielCode, supplierCode = lockInfo.SupplyCode, quantityTotal = splitQty.ToString("F2"), batchNumber = newBarcode, batch = lockInfo.BatchNo, factory = lockInfo.FactoryArea, date = DateTime.Now.ToString("yyyy-MM-dd"), }, new SplitResult { materialCode = lockInfo.MaterielCode, supplierCode = lockInfo.SupplyCode, quantityTotal = remainQty.ToString("F2"), batchNumber = originalBarcode, batch = lockInfo.BatchNo, factory = lockInfo.FactoryArea, date = DateTime.Now.ToString("yyyy-MM-dd"), } }; } private async Task UpdateSplitRecordsStatus(string barcode) { 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(); } } private async Task GenerateTaskNumber() { return await _dailySequenceService.GetNextSequenceAsync(); } private WebResponseContent CreatePickingResponse(PickingResult result, string adjustedReason) { //if (result.SplitResults.Any()) //{ // var responseData = new { SplitResults = result.SplitResults, AdjustedReason = "" }; // if (!string.IsNullOrEmpty(adjustedReason)) // { // responseData = new { SplitResults = result.SplitResults, AdjustedReason = adjustedReason }; // } // return WebResponseContent.Instance.OK("拣选确认成功,已自动拆包", responseData); //} //if (!string.IsNullOrEmpty(adjustedReason)) //{ // return WebResponseContent.Instance.OK($"拣选确认成功({adjustedReason})"); //} //return WebResponseContent.Instance.OK("拣选确认成功"); if (result.SplitResults.Any()) { return WebResponseContent.Instance.OK("拣选确认成功,已自动拆包", new { SplitResults = result.SplitResults }); } return WebResponseContent.Instance.OK("拣选确认成功", new { SplitResults = new List() }); } #endregion } #region 支持类定义 public class ValidationResult { public bool IsValid { get; set; } public T Data { get; set; } public string ErrorMessage { get; set; } public static ValidationResult Success(T data) { return new ValidationResult { IsValid = true, Data = data }; } public static ValidationResult Error(string message) { return new ValidationResult { IsValid = false, ErrorMessage = message }; } } public class PickingResult { public Dt_OutStockLockInfo FinalLockInfo { get; set; } public string FinalBarcode { get; set; } public int FinalStockId { get; set; } public decimal ActualPickedQty { get; set; } public List SplitResults { get; set; } = new List(); } public class ReturnAnalysisResult { public bool HasItemsToReturn { get; set; } public bool HasRemainingLocks { get; set; } public bool HasPalletStockGoods { get; set; } public bool HasSplitRecords { get; set; } public decimal RemainingLocksReturnQty { get; set; } public decimal PalletStockReturnQty { get; set; } public decimal SplitReturnQty { get; set; } public decimal TotalReturnQty { get; set; } public List RemainingLocks { get; set; } = new List(); public List PalletStockGoods { get; set; } = new List(); public List SplitRecords { get; set; } = new List(); } public class PalletStatusAnalysis { public string OrderNo { get; set; } public string PalletCode { get; set; } public int StockId { get; set; } // 回库相关属性 public bool HasItemsToReturn { get; set; } public bool HasRemainingLocks { get; set; } public bool HasPalletStockGoods { get; set; } public bool HasSplitRecords { get; set; } public decimal RemainingLocksReturnQty { get; set; } public decimal PalletStockReturnQty { get; set; } public decimal SplitReturnQty { get; set; } public decimal TotalReturnQty { get; set; } public List RemainingLocks { get; set; } = new List(); public List PalletStockGoods { get; set; } = new List(); public List SplitRecords { get; set; } = new List(); // 空托盘相关属性 public bool IsEmptyPallet { get; set; } public bool HasActiveTasks { get; set; } // 便利方法 public bool CanReturn => HasItemsToReturn && !HasActiveTasks; public bool CanRemove => IsEmptyPallet && !HasActiveTasks; } #endregion }