using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using SqlSugar; 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.CodeConfigEnum; using WIDESEA_Core.Helper; using WIDESEA_DTO.CalcOut; using WIDESEA_DTO.ReturnMES; using WIDESEA_IBasicService; using WIDESEA_IOutboundService; using WIDESEA_IRecordService; using WIDESEA_IStockService; using WIDESEA_Model.Models; namespace WIDESEA_OutboundService { public partial class OutboundService : IOutboundService { public IUnitOfWorkManage _unitOfWorkManage { get; } public IOutboundOrderDetailService OutboundOrderDetailService { get; } public IOutboundOrderService OutboundOrderService { get; } public IOutStockLockInfoService OutboundStockLockInfoService { get; } private readonly ISqlSugarClient Db; private readonly IBasicService _basicService; private readonly IRepository _detailRepository; private readonly IRepository _outboundRepository; private readonly IRepository _outboundLockInfoRepository; private readonly IRepository _stockInfoRepository; private readonly IRepository _stockDetailRepository; private readonly IRepository _locationInfoRepository; private readonly IRepository _stockChangeRepository; private readonly IRepository _stockDetailHistoryRepository; public OutboundService(IUnitOfWorkManage unitOfWorkManage, IRepository detailRepository, IRepository outboundRepository, IRepository outboundLockInfoRepository, IRepository stockInfoRepository, IRepository stockDetailRepository, IRepository stockChangeRepository, IRepository stockDetailHistoryRepository, IBasicService basicService, IOutboundOrderDetailService outboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutStockLockInfoService outboundStockLockInfoService) { _unitOfWorkManage = unitOfWorkManage; Db = _unitOfWorkManage.GetDbClient(); OutboundOrderDetailService = outboundOrderDetailService; OutboundOrderService = outboundOrderService; OutboundStockLockInfoService = outboundStockLockInfoService; _detailRepository = detailRepository; _outboundRepository = outboundRepository; _outboundLockInfoRepository = outboundLockInfoRepository; _stockInfoRepository = stockInfoRepository; _stockDetailRepository = stockDetailRepository; _locationInfoRepository = basicService.LocationInfoService.Repository; _stockChangeRepository = stockChangeRepository; _stockDetailHistoryRepository = stockDetailHistoryRepository; _basicService = basicService; } /// /// 分拣出库操作 /// /// 分拣出库请求 /// 分拣出库响应 public WebResponseContent ProcessPickingOutbound(PickingOutboundRequestDTO request) { WebResponseContent content = WebResponseContent.Instance; PickingOutboundResponseDTO response = new PickingOutboundResponseDTO(); try { // 1. 计算出库数量逻辑 OutboundCalculationDTO calculationResult = CalcOutboundQuantity(request); if (!calculationResult.CanOutbound) { content = WebResponseContent.Instance.Error("无法处理拣货出库:" + calculationResult.ErrorMessage); _unitOfWorkManage.RollbackTran(); return content; } // 2. 调用出库处理逻辑,锁定库存,生成出库记录等 List pickedDetails = new List(); // 获取出库单信息 Dt_OutboundOrder outboundOrder = calculationResult.OutboundOrder; // 出库详情添加或修改集合 List outStockLockInfos = new List(); List outboundOrderDetails = new(); List tasks = new List(); foreach (var materielCalc in calculationResult.MaterielCalculations) { (List PickedDetails, List Tasks, List OutStockLockInfo) materielPickedDetails = ProcessMaterielTaskGeneration(outboundOrder, materielCalc, request, calculationResult.FactoryArea); foreach (var item in materielPickedDetails.OutStockLockInfo) { Dt_OutStockLockInfo? outStockLockInfo = materielCalc.OutStockLockInfos.FirstOrDefault(x => x.Id == item.Id && x.Id > 0); if (outStockLockInfo != null) { outStockLockInfo = item; Dt_Task? task = tasks.FirstOrDefault(x => x.PalletCode == item.PalletCode); if (task != null) { outStockLockInfo.TaskNum = task.TaskNum; } } else { Dt_Task? task = tasks.FirstOrDefault(x => x.PalletCode == item.PalletCode); if (task != null) { item.TaskNum = task.TaskNum; } materielCalc.OutStockLockInfos.Add(item); } outStockLockInfos.Add(item); } foreach (var item in materielPickedDetails.Tasks) { Dt_Task? task = tasks.FirstOrDefault(x => x.PalletCode == item.PalletCode); if (task == null) { tasks.Add(item); } } pickedDetails.AddRange(materielPickedDetails.PickedDetails); // 更新出库单明细(增加锁定数量,不增加已出数量) foreach (var detail in materielCalc.Details) { decimal lockQuantity = (detail.OrderQuantity - detail.OverOutQuantity); if (lockQuantity < materielCalc.UnallocatedQuantity) { detail.LockQuantity += lockQuantity; // 增加锁定数量 不更新 OverOutQuantity 和 OrderDetailStatus,因为还没有实际出库 outboundOrderDetails.Add(detail); } else { detail.LockQuantity += materielCalc.UnallocatedQuantity; outboundOrderDetails.Add(detail); break; } } } // 3. 更新出库单状态为出库中(表示已有任务分配) UpdateOutboundOrderStatus(request.OrderNo, (int)OutOrderStatusEnum.出库中); // 4. 更新出库单明细锁定数量 _detailRepository.UpdateData(outboundOrderDetails); // 5. 更新库存状态 UpdateStockStatus(pickedDetails.Select(x => x.PalletCode).ToList(), StockStatusEmun.出库锁定.ObjToInt()); // 6. 更新货位状态 UpdateLocationStatus(pickedDetails.Select(x => x.LocationCode).ToList(), LocationStatusEnum.Lock.ObjToInt()); // 7. 更新库存详情 UpdateOutStockLockInfo(outStockLockInfos); _unitOfWorkManage.CommitTran(); response.Success = true; response.Message = "分拣任务分配成功"; response.Tasks = tasks; // 返回第一个任务号 response.PickedDetails = pickedDetails; // 返回第一个分拣明细 content = WebResponseContent.Instance.OK("分拣任务分配成功", response); return content; } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); content = WebResponseContent.Instance.Error("处理拣货出库失败:" + ex.Message); } return content; } /// /// 计算出库数量逻辑 /// /// /// public OutboundCalculationDTO CalcOutboundQuantity(PickingOutboundRequestDTO request) { OutboundCalculationDTO result = new(); try { Dt_OutboundOrder outboundOrder = _outboundRepository.QueryFirst(x => x.OrderNo == request.OrderNo); if (outboundOrder == null) { result.CanOutbound = false; result.ErrorMessage = $"出库单 {request.OrderNo} 不存在"; return result; } result.FactoryArea = outboundOrder.FactoryArea; // 获取选择的出库明细 List selectedDetails = _detailRepository.QueryData(x => x.OrderId == outboundOrder.Id && request.DetailIds.Contains(x.Id)); if (outboundOrder.IsBatch == 1) { selectedDetails = _detailRepository.QueryData(x => x.WarehouseCode == selectedDetails.First().WarehouseCode && x.MaterielCode == selectedDetails.First().MaterielCode && x.BatchNo == selectedDetails.First().BatchNo && x.SupplyCode == selectedDetails.First().SupplyCode); } if (!selectedDetails.Any()) { result.CanOutbound = false; result.ErrorMessage = $"未找到选择的出库明细信息"; return result; } if (selectedDetails.Any(x => x.LockQuantity > x.OrderQuantity - x.MoveQty || x.OverOutQuantity > x.OrderQuantity - x.MoveQty)) { List selectDetailIds = selectedDetails.Where(x => x.LockQuantity > x.OrderQuantity - x.MoveQty || x.OverOutQuantity > x.OrderQuantity - x.MoveQty).Select(x => x.Id).ToList(); result.CanOutbound = false; result.ErrorMessage = $"出库明细信息{string.Join(",", selectDetailIds)}已分配完成"; return result; } outboundOrder.Details = selectedDetails; result.OutboundOrder = outboundOrder; result.SelectedDetails = selectedDetails; if (outboundOrder.IsBatch == 0) { // 多明细出库:按物料分组处理 result.MaterielCalculations = CalcMaterielOutboundQuantities(outboundOrder, selectedDetails.ToList()); } else { // 单明细出库:验证输入的出库数量 if (!request.OutboundQuantity.HasValue || request.OutboundQuantity.Value <= 0) { result.CanOutbound = false; result.ErrorMessage = "单明细出库时必须指定出库数量且大于0"; return result; } decimal lockQuantity = selectedDetails.Sum(x => x.LockQuantity); decimal orderQuantity = selectedDetails.Sum(x => x.OrderQuantity); decimal moveQuantity = selectedDetails.Sum(x => x.MoveQty); decimal overQuantity = selectedDetails.Sum(x => x.OverOutQuantity); Dt_OutboundOrderDetail? singleDetail = selectedDetails.First(); //判断可出库数量 if (orderQuantity - lockQuantity - moveQuantity < request.OutboundQuantity.Value || orderQuantity - overQuantity - moveQuantity < request.OutboundQuantity.Value) { result.CanOutbound = false; result.ErrorMessage = $"本次出库数量 {request.OutboundQuantity.Value} 超过可出库数量 {orderQuantity - lockQuantity - moveQuantity}"; return result; } result.MaterielCalculations = new List() { new MaterielOutboundCalculationDTO { MaterielCode = singleDetail.MaterielCode, MaterielName = singleDetail.MaterielName, BatchNo = singleDetail.BatchNo, SupplyCode = singleDetail.SupplyCode, WarehouseCode = singleDetail.WarehouseCode, TotalOrderQuantity = orderQuantity - moveQuantity, TotalOverOutQuantity = overQuantity, AssignedQuantity = lockQuantity, UnallocatedQuantity = request.OutboundQuantity.Value, MovedQuantity = moveQuantity, Details = selectedDetails } }; } result.CanOutbound = true; return result; } catch (Exception ex) { result.CanOutbound = false; result.ErrorMessage = ex.Message; return result; } } /// /// 多出库单明细时,按物料分组计算出库数量 /// /// /// private List CalcMaterielOutboundQuantities(Dt_OutboundOrder outboundOrder, List selectedDetails) { // 按物料分组:物料编号、批次号、供应商编号、仓库编号 List materielGroups = selectedDetails .GroupBy(x => new { x.MaterielCode, x.MaterielName, x.BatchNo, x.SupplyCode, x.WarehouseCode }) .Select(g => new MaterielOutboundCalculationDTO { MaterielCode = g.Key.MaterielCode, MaterielName = g.Key.MaterielName, BatchNo = g.Key.BatchNo, SupplyCode = g.Key.SupplyCode, WarehouseCode = g.Key.WarehouseCode, TotalOrderQuantity = g.Sum(x => x.OrderQuantity - x.MoveQty), TotalOverOutQuantity = g.Sum(x => x.OverOutQuantity), AssignedQuantity = g.Sum(x => x.LockQuantity), UnallocatedQuantity = g.Sum(x => x.OrderQuantity - x.LockQuantity - x.MoveQty), MovedQuantity = g.Sum(x => x.MoveQty), Details = g.ToList(), OutStockLockInfos = _outboundLockInfoRepository.QueryData(x => x.MaterielCode == g.Key.MaterielCode && x.BatchNo == g.Key.BatchNo && x.OrderType == (int)outboundOrder.OrderType && x.OrderNo == outboundOrder.OrderNo) }) .ToList(); return materielGroups; } /// /// 处理物料的任务生成 /// /// 出库订单 /// 按物料的出库计算结果 /// 分拣出库请求 /// /// /// private (List PickedDetails, List Tasks, List OutStockLockInfo) ProcessMaterielTaskGeneration(Dt_OutboundOrder outboundOrder, MaterielOutboundCalculationDTO materielCalc, PickingOutboundRequestDTO request, string factoryArea) { List pickedDetails = new List(); List generatedTasks = new List(); // 构建库存查询条件(包含库存表、库存明细) List stockQuery = BuildStockQueryWithInfo(materielCalc, factoryArea); if (!stockQuery.Any()) { throw new Exception($"物料 {materielCalc.MaterielCode} 对应的库存不存在"); } // 批量计算总可用库存数量 (Dictionary AvailableStockMap, Dictionary> LockStockMap) data = GetBatchAvailableStockQuantities(materielCalc, stockQuery); // 可用库存数量映射 Dictionary availableStockMap = data.AvailableStockMap; // 物料总可用库存数量 decimal totalAvailableStock = availableStockMap.Values.Sum(); // 已锁定库存数量映射 Dictionary> lockStockMap = data.LockStockMap; // 验证总可用库存是否满足出库需求 if (totalAvailableStock < materielCalc.UnallocatedQuantity) { throw new Exception($"物料 {materielCalc.MaterielCode} 可用库存 {totalAvailableStock} 不足出库数量 {materielCalc.UnallocatedQuantity}"); } // 需分配数量 decimal remainingQuantity = materielCalc.UnallocatedQuantity; // 已分配的托盘列表 List allocatedPallets = new List(); // 获取第一个出库明细 Dt_OutboundOrderDetail firstDetail = materielCalc.Details.First(); // 预加载库存明细和锁定记录 List stockIds = stockQuery.Select(x => x.Id).ToList(); Dictionary> stockDetailMap = stockQuery.ToDictionary(x => x.Id, x => x.Details); // 记录每个托盘的实际分配量 Dictionary palletAllocations = new Dictionary(); List lockInfoList = new List(); foreach (var stock in stockQuery) { if (remainingQuantity <= 0) break; // 当前库存可用数量 decimal availableQuantity = availableStockMap.GetValueOrDefault(stock.Id, 0); if (availableQuantity <= 0) continue; // 计算该托盘可分配数量 decimal allocateQuantity = Math.Min(remainingQuantity, availableQuantity); (decimal ActualAllocatedQuantity, List LockInfoList) actualAllocated = AllocateStockQuantity(stock, allocateQuantity, availableQuantity, outboundOrder, firstDetail, request, lockStockMap.GetValueOrDefault(stock.Id, new List()), stockDetailMap); // 本次分配的数量 decimal actualAllocatedQuantity = actualAllocated.ActualAllocatedQuantity; if (actualAllocatedQuantity > 0) { allocatedPallets.Add(stock.PalletCode); palletAllocations[stock.PalletCode] = actualAllocatedQuantity; // 记录实际分配量 remainingQuantity -= actualAllocatedQuantity; lockInfoList.AddRange(actualAllocated.LockInfoList); } } foreach (var palletCode in allocatedPallets) { Dt_StockInfo stock = stockQuery.First(x => x.PalletCode == palletCode); // 获取实际分配的数量 decimal actualAllocatedQuantity = palletAllocations.GetValueOrDefault(palletCode, 0); // 计算分配后剩余的库存数量 decimal originalAvailableQuantity = availableStockMap.GetValueOrDefault(stock.Id, 0); decimal remainingStockQuantity = Math.Max(0, originalAvailableQuantity - actualAllocatedQuantity); pickedDetails.Add(new PickedStockDetailDTO { PalletCode = palletCode, MaterielCode = materielCalc.MaterielCode, OutboundQuantity = actualAllocatedQuantity, // 本次实际分配的出库量 RemainingQuantity = remainingStockQuantity, // 分配后剩余的可用库存 LocationCode = stock.LocationCode, OutStockLockInfos = lockInfoList }); Dt_OutStockLockInfo? outStockLockInfo = lockInfoList.FirstOrDefault(x => x.PalletCode == palletCode); int taskNum = outStockLockInfo?.TaskNum ?? Db.Ado.GetScalar($"SELECT NEXT VALUE FOR SeqTaskNum").ObjToInt(); Dt_Task task = GenerationOutTask(stock, TaskTypeEnum.Outbound, taskNum, request.OutboundTargetLocation); if (generatedTasks.FirstOrDefault(x => x.PalletCode == stock.PalletCode) == null) generatedTasks.Add(task); } return (pickedDetails, generatedTasks, lockInfoList); } /// /// 生成出库任务 /// /// /// /// /// public Dt_Task GenerationOutTask(Dt_StockInfo stockInfo, TaskTypeEnum taskType, int taskNum, string outStation) { Dt_Task task = new() { CurrentAddress = stockInfo.LocationCode, Grade = 0, PalletCode = stockInfo.PalletCode, NextAddress = outStation, Roadway = "", SourceAddress = stockInfo.LocationCode, TargetAddress = outStation, TaskStatus = TaskStatusEnum.New.ObjToInt(), TaskType = taskType.ObjToInt(), TaskNum = taskNum, PalletType = stockInfo.PalletType, WarehouseId = stockInfo.WarehouseId, }; return task; } /// /// 构建库存查询条件(包含库存信息和库存明细) /// /// /// /// private List BuildStockQueryWithInfo(MaterielOutboundCalculationDTO materielCalc, string factoryArea) { // 基础查询条件:物料编号、批次号(如果提供)、库存数量>0 ISugarQueryable stockDetails = _stockDetailRepository.Db.Queryable().Where(x => x.MaterielCode == materielCalc.MaterielCode && x.StockQuantity > 0); // 根据条件添加供应商编号匹配(不为空时才需要匹配) if (!string.IsNullOrEmpty(materielCalc.SupplyCode)) { stockDetails = stockDetails.Where(x => x.SupplyCode == materielCalc.SupplyCode); } // 根据条件添加仓库编号匹配(不为空时才需要匹配) if (!string.IsNullOrEmpty(materielCalc.WarehouseCode)) { stockDetails = stockDetails.Where(x => x.WarehouseCode == materielCalc.WarehouseCode); } // 根据条件添加厂区匹配(不为空时才需要匹配) if (!string.IsNullOrEmpty(factoryArea)) { stockDetails = stockDetails.Where(x => x.FactoryArea == factoryArea); } // 根据批次号进行过滤(如果提供) if (!string.IsNullOrEmpty(materielCalc.BatchNo)) { stockDetails = stockDetails.Where(x => x.BatchNo == materielCalc.BatchNo); } List stockDetailList = stockDetails.ToList(); // 获取可用货位编号 List locationCodes = _locationInfoRepository.QueryData(x => (x.LocationStatus == LocationStatusEnum.InStock.ObjToInt() /*|| x.LocationStatus == LocationStatusEnum.Lock.ObjToInt()*/) && x.EnableStatus == EnableStatusEnum.Normal.ObjToInt()).Select(x => x.LocationCode).ToList(); // 获取所有相关的库存信息 List stockIds = stockDetailList.GroupBy(x => x.StockId).Select(x => x.Key).ToList(); List stockInfos = _stockInfoRepository.QueryData(x => stockIds.Contains(x.Id) && (x.StockStatus == StockStatusEmun.入库完成.ObjToInt() /*|| x.StockStatus == StockStatusEmun.出库锁定.ObjToInt()*/) && !string.IsNullOrEmpty(x.LocationCode) && locationCodes.Contains(x.LocationCode)); // 在内存中关联数据 foreach (var stockInfo in stockInfos) { stockInfo.Details = new List(); stockInfo.Details = stockDetailList.Where(x => x.StockId == stockInfo.Id).ToList(); } return stockInfos; } /// /// 批量获取托盘可用库存信息 /// /// 库存信息列表 /// 物料编号 /// 返回值为(库存主键,可用数量)字典 private (Dictionary AvailableStockMap, Dictionary> LockStockMap) GetBatchAvailableStockQuantities(MaterielOutboundCalculationDTO materielCalc, List stockInfos) { List stockIds = stockInfos.Select(x => x.Id).ToList(); Dictionary availableStockMap = new Dictionary(); // 可用库存数量 Dictionary> lockStockMap = new Dictionary>(); // 已锁定库存数量 // 批量查询已分配数量 List allocatedData = materielCalc.OutStockLockInfos.Where(x => stockIds.Contains(x.StockId) && x.MaterielCode == materielCalc.MaterielCode).ToList(); foreach (var stockInfo in stockInfos) { // 计算总库存数量 decimal totalQuantity = stockInfo.Details.Sum(x => x.StockQuantity); List outStockLockInfos = allocatedData.Where(x => x.StockId == stockInfo.Id && x.MaterielCode == materielCalc.MaterielCode).ToList(); // 计算已分配数量 decimal allocatedQuantity = outStockLockInfos.Sum(x => x.AllocatedQuantity); availableStockMap[stockInfo.Id] = Math.Max(0, totalQuantity - allocatedQuantity); lockStockMap[stockInfo.Id] = outStockLockInfos; } return (availableStockMap, lockStockMap); } /// /// 分配库存 /// /// 库存信息 /// 要分配的数量 /// 可分配的数量 /// 出库单 /// 出库单明细 /// /// /// /// private (decimal ActualAllocatedQuantity, List LockInfoList) AllocateStockQuantity(Dt_StockInfo stockInfo, decimal allocateQuantity, decimal availableQuantity, Dt_OutboundOrder outboundOrder, Dt_OutboundOrderDetail detail, PickingOutboundRequestDTO request, List lockInfos, Dictionary> stockDetailMap = null) { decimal actualAllocatedQuantity = Math.Min(allocateQuantity, availableQuantity); // 实际分配数量 List lockInfoList = new List(); if (actualAllocatedQuantity > 0) { //检查目标位置一致性:如果托盘已有锁定记录且目标位置不同,则不允许分配 if (lockInfos.Any() && !string.IsNullOrEmpty(lockInfos.First().OutboundTargetLocation)) { if (!string.Equals(lockInfos.First().OutboundTargetLocation, request.OutboundTargetLocation, StringComparison.OrdinalIgnoreCase)) { // 托盘的目标位置与新请求的目标位置不同,不允许使用该托盘 return (0, lockInfoList); } } Dt_OutStockLockInfo? lockInfo = lockInfos.FirstOrDefault(x => x.StockId == stockInfo.Id && x.Status == OutLockStockStatusEnum.已分配.ObjToInt() && x.PalletCode == stockInfo.PalletCode && x.OrderNo == outboundOrder.OrderNo); if (lockInfo != null) { // 追加当前明细ID到OrderDetailIds字段(避免重复) List currentIds = lockInfo.OrderDetailIds?.Split(',').ToList() ?? new List(); if (!currentIds.Contains(detail.Id.ToString())) { currentIds.Add(detail.Id.ToString()); lockInfo.OrderDetailIds = string.Join(",", currentIds); } // 计算该托盘该物料的总累计已出库数量(跨所有单据) decimal totalAllocatedQuantity = CalcTotalAllocatedQuantity(lockInfos, stockInfo.Id, detail.MaterielCode); // 更新分配出库量 decimal beforeAssignQuantity = totalAllocatedQuantity; // 本次分配前的总累计量 lockInfo.AssignQuantity += actualAllocatedQuantity; // 本次分配数量 lockInfo.AllocatedQuantity = beforeAssignQuantity; // 记录本次分配前的总累计量 lockInfoList.Add(lockInfo); } else { // 创建新的锁定记录(使用预加载的库存明细) decimal originalQuantity = 0; if (stockDetailMap?.ContainsKey(stockInfo.Id) == true) { originalQuantity = stockDetailMap[stockInfo.Id].Sum(x => x.StockQuantity); } // 获取该物料在该订单中的所有明细ID List allDetailIds = (outboundOrder.Details.Where(x => x.OrderId == outboundOrder.Id && x.MaterielCode == detail.MaterielCode && x.BatchNo == detail.BatchNo && x.SupplyCode == detail.SupplyCode && x.WarehouseCode == detail.WarehouseCode)) .Select(x => x.Id.ToString()) .ToList(); // 计算该托盘该物料的总累计已出库数量(跨所有单据) decimal totalAllocatedQuantity = CalcTotalAllocatedQuantity(lockInfos, stockInfo.Id, detail.MaterielCode); lockInfo = new Dt_OutStockLockInfo { OrderNo = request.OrderNo, OrderDetailIds = string.Join(",", allDetailIds), // 记录该物料的所有明细ID OrderType = outboundOrder.OrderType, BatchNo = detail.BatchNo, MaterielCode = detail.MaterielCode, MaterielName = detail.MaterielName, StockId = stockInfo.Id, OrderQuantity = allDetailIds.SelectMany(id => allDetailIds).Count() > 1 ? (outboundOrder.Details.Where(x => x.OrderId == outboundOrder.Id && x.MaterielCode == detail.MaterielCode && x.BatchNo == detail.BatchNo && x.SupplyCode == detail.SupplyCode && x.WarehouseCode == detail.WarehouseCode)) .Sum(x => x.OrderQuantity) : detail.OrderQuantity, // 如果只有一个明细,使用明细数量 OriginalQuantity = originalQuantity, AssignQuantity = actualAllocatedQuantity, // 本次分配数量 AllocatedQuantity = totalAllocatedQuantity, // 本次分配前的总累计已出库数量(跨所有单据) LocationCode = stockInfo.LocationCode, PalletCode = stockInfo.PalletCode, Unit = detail.Unit, OutboundTargetLocation = request.OutboundTargetLocation, Status = OutLockStockStatusEnum.已分配.ObjToInt(), SupplyCode = detail.SupplyCode, WarehouseCode = detail.WarehouseCode, FactoryArea = outboundOrder.FactoryArea, TaskNum = Db.Ado.GetScalar($"SELECT NEXT VALUE FOR SeqTaskNum").ObjToInt(), OrderDetailId = 0 // 未关联具体明细ID }; lockInfoList.Add(lockInfo); } } return (actualAllocatedQuantity, lockInfoList); } /// /// 计算该托盘累计已分配数量 /// /// /// /// /// private decimal CalcTotalAllocatedQuantity(List lockInfos, int stockId, string materielCode) { // 查询该托盘该物料在所有锁定记录中的最大已分配数量 List lockRecords = _outboundLockInfoRepository.QueryData(x => x.StockId == stockId && x.MaterielCode == materielCode); if (lockRecords == null || !lockRecords.Any()) { return 0; } // 返回累计已分配数量 return lockRecords.Sum(x => x.AssignQuantity); } /// /// 更新出库单状态 /// public bool UpdateOutboundOrderStatus(string orderNo, int status) { try { Dt_OutboundOrder outboundOrder = _outboundRepository.QueryFirst(x => x.OrderNo == orderNo); if (outboundOrder == null) return false; outboundOrder.OrderStatus = status; _outboundRepository.UpdateData(outboundOrder); return true; } catch { return false; } } /// /// /// /// /// /// public bool UpdateStockStatus(List palletCodes, int status) { try { List stockInfos = _stockInfoRepository.QueryData(x => palletCodes.Contains(x.PalletCode)); stockInfos.ForEach(stockInfo => { stockInfo.StockStatus = status; }); _stockInfoRepository.UpdateData(stockInfos); return true; } catch { return false; } } /// /// /// /// /// /// public bool UpdateLocationStatus(List locationCodes, int status) { try { List locationInfos = _locationInfoRepository.QueryData(x => locationCodes.Contains(x.LocationCode)); locationInfos.ForEach(x => { x.LocationStatus = status; }); _locationInfoRepository.UpdateData(locationInfos); return true; } catch { return false; } } /// /// /// /// /// public bool UpdateOutStockLockInfo(List outStockLockInfos) { try { List updateData = outStockLockInfos.Where(x => x.Id > 0).ToList(); _outboundLockInfoRepository.UpdateData(updateData); List addData = outStockLockInfos.Where(x => x.Id <= 0).ToList(); _outboundLockInfoRepository.AddData(addData); return true; } catch { return false; } } /// /// 出库完成处理(扫描条码扣减库存) /// /// 出库完成请求 /// 出库完成响应 public WebResponseContent CompleteOutboundWithBarcode(OutboundCompleteRequestDTO request) { WebResponseContent content = WebResponseContent.Instance; OutboundCompleteResponseDTO response = new(); try { // 1. 根据托盘号查找库存信息 Dt_StockInfo stockInfo = _stockInfoRepository.QueryFirst(x => x.PalletCode == request.PalletCode); if (stockInfo == null) { response.Success = false; response.Message = $"托盘号 {request.PalletCode} 对应的库存不存在"; return WebResponseContent.Instance.Error($"托盘号 {request.PalletCode} 对应的库存不存在"); } // 2. 根据条码查找库存明细 Dt_StockInfoDetail stockDetail = _stockDetailRepository.QueryFirst(x => x.Barcode == request.Barcode); if (stockDetail == null) { response.Success = false; response.Message = $"条码 {request.Barcode} 对应的库存明细不存在"; return WebResponseContent.Instance.Error($"条码 {request.Barcode} 对应的库存明细不存在"); } // 3. 验证库存明细与托盘是否匹配 if (stockDetail.StockId != stockInfo.Id) { response.Success = false; response.Message = $"条码 {request.Barcode} 不属于托盘号 {request.PalletCode} 的库存明细"; return WebResponseContent.Instance.Error($"条码 {request.Barcode} 不属于托盘号 {request.PalletCode} 的库存明细"); } // 4. 查找出库单信息 Dt_OutboundOrder outboundOrder = _outboundRepository.QueryFirst(o => o.OrderNo == request.OrderNo); if (outboundOrder == null) { response.Success = false; response.Message = $"出库单 {request.OrderNo} 不存在"; return WebResponseContent.Instance.Error($"出库单 {request.OrderNo} 不存在"); } // 5. 查找锁定记录 Dt_OutStockLockInfo lockInfo = _outboundLockInfoRepository.QueryFirst(x => x.OrderNo == request.OrderNo && x.StockId == stockInfo.Id && x.MaterielCode == stockDetail.MaterielCode && x.PalletCode == stockInfo.PalletCode); if (lockInfo == null || lockInfo.AssignQuantity <= 0) { response.Success = false; response.Message = $"该库存没有分配出库量,条码:{request.Barcode}"; return WebResponseContent.Instance.Error($"该库存没有分配出库量,条码:{request.Barcode}"); } // 找出已分配的订单明细Id List detailIds = new List(); string[] ids = lockInfo.OrderDetailIds.Split(","); foreach (string id in ids) { if (int.TryParse(id, out int detailId)) { detailIds.Add(detailId); } } // 6. 查找出库单明细信息 List outboundOrderDetails = FindMatchingOutboundDetails(outboundOrder.Id, stockDetail, detailIds); if (!outboundOrderDetails.Any()) { response.Success = false; response.Message = $"未找到匹配的出库单明细,物料:{stockDetail.MaterielCode},批次:{stockDetail.BatchNo}"; return WebResponseContent.Instance.Error($"未找到匹配的出库单明细,物料:{stockDetail.MaterielCode},批次:{stockDetail.BatchNo}"); } // 7. 计算实际出库量 decimal actualOutboundQuantity = CalculateActualOutboundQuantity(stockDetail, outboundOrderDetails, lockInfo);// 需出库量 if (actualOutboundQuantity <= 0) { decimal totalAllocatedQuantity = lockInfo.AllocatedQuantity; decimal availableOutboundQuantity = lockInfo.AssignQuantity - totalAllocatedQuantity; decimal detailRemainingQuantity = outboundOrderDetails.Sum(x => x.OrderQuantity - x.OverOutQuantity - x.MoveQty); response.Success = false; response.Message = $"无法出库,条码:{request.Barcode},库存:{stockDetail.StockQuantity},已出库:{totalAllocatedQuantity},分配量:{lockInfo.AssignQuantity},明细剩余:{detailRemainingQuantity}"; return WebResponseContent.Instance.Error($"无法出库,条码:{request.Barcode},库存:{stockDetail.StockQuantity},已出库:{totalAllocatedQuantity},分配量:{lockInfo.AssignQuantity},明细剩余:{detailRemainingQuantity}"); } if (actualOutboundQuantity + lockInfo.SortedQuantity > lockInfo.AssignQuantity) { response.Success = false; response.Message = $"无法出库,条码:{request.Barcode},库存:{stockDetail.StockQuantity},出库量{actualOutboundQuantity + lockInfo.SortedQuantity}大于分配量{lockInfo.AssignQuantity}"; return WebResponseContent.Instance.Error($"无法出库,条码:{request.Barcode},库存:{stockDetail.StockQuantity},出库量{actualOutboundQuantity + lockInfo.SortedQuantity}大于分配量{lockInfo.AssignQuantity}"); } // 8. 判断是否需要拆包(当出库数量小于库存数量时需要拆包) bool isUnpacked = actualOutboundQuantity < stockDetail.StockQuantity; string newBarcode = string.Empty; // 9. 开启事务 _unitOfWorkManage.BeginTran(); try { decimal beforeQuantity = stockDetail.StockQuantity; // 原始库存量 // 根据是否拆包执行不同的操作 if (isUnpacked) { newBarcode = PerformUnpackOperation(stockDetail, stockInfo, actualOutboundQuantity, request, beforeQuantity, lockInfo.TaskNum.GetValueOrDefault()); } else { PerformFullOutboundOperation(stockDetail, stockInfo, actualOutboundQuantity, request, beforeQuantity, lockInfo.TaskNum.GetValueOrDefault()); } decimal allocatedQuantity = actualOutboundQuantity; List updateDetails = new(); foreach (var item in outboundOrderDetails) { if (allocatedQuantity <= 0) break; //if (item.OrderQuantity - item.MoveQty - item.OverOutQuantity >= allocatedQuantity) //{ // item.OverOutQuantity += allocatedQuantity; // allocatedQuantity = 0; //} //else //{ // allocatedQuantity -= (item.OrderQuantity - item.MoveQty - item.OverOutQuantity); // item.OverOutQuantity = item.OrderQuantity - item.MoveQty; //} if (item.LockQuantity - item.OverOutQuantity >= allocatedQuantity) { item.OverOutQuantity += allocatedQuantity; item.CurrentDeliveryQty += allocatedQuantity; allocatedQuantity = 0; } else { allocatedQuantity -= (item.LockQuantity - item.OverOutQuantity); item.OverOutQuantity = item.LockQuantity; item.CurrentDeliveryQty = item.LockQuantity; } updateDetails.Add(item); List barcodesList = new List(); Barcodes barcodes = new Barcodes { Barcode = request.Barcode, Qty = actualOutboundQuantity, SupplyCode = stockDetail?.SupplyCode ?? "", BatchNo = stockDetail?.BatchNo ?? "", Unit = stockDetail?.Unit ?? "" }; if (!string.IsNullOrEmpty(item.ReturnJsonData)) { barcodesList = JsonConvert.DeserializeObject>(item.ReturnJsonData) ?? new List(); } barcodesList.Add(barcodes); JsonSerializerSettings settings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; item.ReturnJsonData = JsonConvert.SerializeObject(barcodesList, settings); } lockInfo.SortedQuantity = lockInfo.SortedQuantity + actualOutboundQuantity; if (lockInfo.SortedQuantity == lockInfo.AssignQuantity) { _outboundLockInfoRepository.DeleteAndMoveIntoHty(lockInfo, WIDESEA_Core.Enums.OperateTypeEnum.自动完成); } else { // 更新锁定记录 _outboundLockInfoRepository.UpdateData(lockInfo); } // 更新出库单明细的已出库数量 _detailRepository.UpdateData(updateDetails); // 更新锁定记录的累计已出库数量(需要更新该托盘该物料的所有相关记录) //UpdateLockInfoAllocatedQuantity(stockInfo.Id, stockDetail.MaterielCode, stockDetail.BatchNo, actualOutboundQuantity); // 提交事务 _unitOfWorkManage.CommitTran(); // 构建返回信息 ScannedStockDetailDTO scannedDetail = new ScannedStockDetailDTO { StockDetailId = stockDetail.Id, PalletCode = stockInfo.PalletCode, MaterielCode = stockDetail.MaterielCode, MaterielName = stockDetail.MaterielName, BatchNo = stockDetail.BatchNo, OriginalBarcode = request.Barcode, BeforeQuantity = beforeQuantity, AfterQuantity = isUnpacked ? actualOutboundQuantity : 0, ChangeQuantity = -actualOutboundQuantity, IsUnpacked = isUnpacked }; response.Success = true; response.Message = isUnpacked ? $"拆包出库完成,已生成新条码:{newBarcode}" : "出库完成"; response.ScannedDetail = scannedDetail; response.UpdatedDetails = updateDetails; response.NewBarcode = newBarcode; // 检查出库单是否完成 if (CheckOutboundOrderCompleted(request.OrderNo)) { UpdateOutboundOrderStatus(request.OrderNo, OutOrderStatusEnum.出库完成.ObjToInt()); //todo: 回传MES } } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); response.Success = false; response.Message = $"出库处理失败:{ex.Message}"; return WebResponseContent.Instance.Error(ex.Message); } content = WebResponseContent.Instance.OK(data: response); } catch (Exception ex) { content = WebResponseContent.Instance.Error("处理出库完成失败:" + ex.Message); } return content; } /// /// /// /// /// /// private List FindMatchingOutboundDetails(int orderId, Dt_StockInfoDetail stockDetail, List detailIds) { List details = _detailRepository.QueryData(x => x.OrderId == orderId && x.MaterielCode == stockDetail.MaterielCode && x.OrderQuantity - x.MoveQty > x.OverOutQuantity && detailIds.Contains(x.Id)); // 精确匹配:处理null值的批次、供应商、仓库 List exactMatches = details.Where(x => (string.IsNullOrEmpty(x.BatchNo)) || x.BatchNo == stockDetail.BatchNo ).Where(x => (string.IsNullOrEmpty(x.SupplyCode)) || x.SupplyCode == stockDetail.SupplyCode ).Where(x => (string.IsNullOrEmpty(x.WarehouseCode)) || x.WarehouseCode == stockDetail.WarehouseCode ).ToList(); return exactMatches; } /// /// 计算实际出库数量 /// private decimal CalculateActualOutboundQuantity(Dt_StockInfoDetail stockDetail, List outboundDetails, Dt_OutStockLockInfo lockInfo) { decimal availableOutboundQuantity = lockInfo.AssignQuantity; decimal detailRemainingQuantity = outboundDetails.Sum(x => x.OrderQuantity - x.OverOutQuantity - x.MoveQty);//outboundDetail.OrderQuantity - outboundDetail.OverOutQuantity; return Math.Min( Math.Min(availableOutboundQuantity, detailRemainingQuantity), stockDetail.StockQuantity); } /// /// 执行拆包操作 /// /// /// /// /// /// /// /// private string PerformUnpackOperation(Dt_StockInfoDetail stockDetail, Dt_StockInfo stockInfo, decimal actualOutboundQuantity, OutboundCompleteRequestDTO request, decimal beforeQuantity, int taskNum) { string newBarcode = GenerateNewBarcode(); // 保存原始库存明细到历史记录 Dt_StockInfoDetail_Hty originalHistoryRecord = new Dt_StockInfoDetail_Hty { SourceId = stockDetail.Id, OperateType = "拆包-原始记录", InsertTime = DateTime.Now, StockId = stockDetail.StockId, MaterielCode = stockDetail.MaterielCode, MaterielName = stockDetail.MaterielName, OrderNo = stockDetail.OrderNo, BatchNo = stockDetail.BatchNo, ProductionDate = stockDetail.ProductionDate, EffectiveDate = stockDetail.EffectiveDate, SerialNumber = stockDetail.SerialNumber, StockQuantity = stockDetail.StockQuantity, OutboundQuantity = stockDetail.OutboundQuantity, Status = stockDetail.Status, Unit = stockDetail.Unit, InboundOrderRowNo = stockDetail.InboundOrderRowNo, SupplyCode = stockDetail.SupplyCode, FactoryArea = stockDetail.FactoryArea, WarehouseCode = stockDetail.WarehouseCode, Remark = $"拆包前原始记录,原条码:{request.Barcode},原数量:{stockDetail.StockQuantity},出库数量:{actualOutboundQuantity},操作者:{request.Operator}" }; _stockDetailHistoryRepository.AddData(originalHistoryRecord); // 保存剩余部分到历史记录 decimal remainingQuantity = stockDetail.StockQuantity - actualOutboundQuantity; if (remainingQuantity > 0) { // 更新原库存明细 stockDetail.StockQuantity = remainingQuantity; //stockDetail.Barcode = newBarcode; stockDetail.Remark = $"拆包后更新,原条码:{request.Barcode},新数量:{remainingQuantity},操作者:{request.Operator}"; _stockDetailRepository.UpdateData(stockDetail); } // 记录拆包变动 Dt_StockQuantityChangeRecord unpackChangeRecord = new Dt_StockQuantityChangeRecord { StockDetailId = stockDetail.Id, PalleCode = stockInfo.PalletCode, MaterielCode = stockDetail.MaterielCode, MaterielName = stockDetail.MaterielName, BatchNo = stockDetail.BatchNo, OriginalSerilNumber = request.Barcode, NewSerilNumber = newBarcode, OrderNo = request.OrderNo, TaskNum = taskNum, ChangeType = (int)StockChangeTypeEnum.Outbound, ChangeQuantity = -actualOutboundQuantity, BeforeQuantity = beforeQuantity, AfterQuantity = beforeQuantity - actualOutboundQuantity, SupplyCode = stockDetail.SupplyCode, WarehouseCode = stockDetail.WarehouseCode, Remark = $"拆包出库,原条码:{request.Barcode},新条码:{newBarcode},出库数量:{actualOutboundQuantity},剩余:{remainingQuantity},操作者:{request.Operator}" }; _stockChangeRepository.AddData(unpackChangeRecord); return newBarcode; } /// /// 执行完整出库操作(不拆包) /// private void PerformFullOutboundOperation(Dt_StockInfoDetail stockDetail, Dt_StockInfo stockInfo, decimal actualOutboundQuantity, OutboundCompleteRequestDTO request, decimal beforeQuantity, int taskNum) { // 保存库存明细到历史记录 Dt_StockInfoDetail_Hty historyRecord = new Dt_StockInfoDetail_Hty { SourceId = stockDetail.Id, OperateType = "出库完成", InsertTime = DateTime.Now, StockId = stockDetail.StockId, MaterielCode = stockDetail.MaterielCode, MaterielName = stockDetail.MaterielName, OrderNo = stockDetail.OrderNo, BatchNo = stockDetail.BatchNo, ProductionDate = stockDetail.ProductionDate, EffectiveDate = stockDetail.EffectiveDate, SerialNumber = stockDetail.SerialNumber, StockQuantity = stockDetail.StockQuantity, OutboundQuantity = stockDetail.OutboundQuantity + actualOutboundQuantity, Status = stockDetail.Status, Unit = stockDetail.Unit, InboundOrderRowNo = stockDetail.InboundOrderRowNo, SupplyCode = stockDetail.SupplyCode, FactoryArea = stockDetail.FactoryArea, WarehouseCode = stockDetail.WarehouseCode, Remark = $"出库完成删除,条码:{request.Barcode},原数量:{stockDetail.StockQuantity},出库数量:{actualOutboundQuantity},操作者:{request.Operator}" }; _stockDetailHistoryRepository.AddData(historyRecord); // 删除库存明细记录 _stockDetailRepository.DeleteData(stockDetail); // 记录库存变动 Dt_StockQuantityChangeRecord changeRecord = new Dt_StockQuantityChangeRecord { StockDetailId = stockDetail.Id, PalleCode = stockInfo.PalletCode, MaterielCode = stockDetail.MaterielCode, MaterielName = stockDetail.MaterielName, BatchNo = stockDetail.BatchNo, OriginalSerilNumber = request.Barcode, NewSerilNumber = "", OrderNo = request.OrderNo, TaskNum = taskNum, ChangeType = (int)StockChangeTypeEnum.Outbound, ChangeQuantity = -actualOutboundQuantity, BeforeQuantity = beforeQuantity, AfterQuantity = 0, SupplyCode = stockDetail.SupplyCode, WarehouseCode = stockDetail.WarehouseCode, Remark = $"出库完成删除库存明细,条码:{request.Barcode},出库数量:{actualOutboundQuantity},操作者:{request.Operator}" }; _stockChangeRepository.AddData(changeRecord); } /// /// 生成新的条码 /// /// 新条码 private string GenerateNewBarcode() { // 使用时间戳和随机数生成唯一条码 string newBarcode = string.Empty; newBarcode = _basicService.CreateCodeByRule(RuleCodeEnum.NewBarcodeRule.ToString()); return newBarcode; } /// /// 更新该托盘该物料的所有锁定记录的累计已出库数量 /// /// 库存ID /// 物料编号 /// 批次号 /// 本次实际出库数量 /// private void UpdateLockInfoAllocatedQuantity(int stockId, string materielCode, string batchNo, decimal actualOutboundQuantity) { // 查询该托盘该物料的所有锁定记录 List lockRecords = _outboundLockInfoRepository.QueryData(x => x.StockId == stockId && x.MaterielCode == materielCode && x.BatchNo == batchNo); if (lockRecords != null && lockRecords.Any()) { // 更新所有相关记录的AllocatedQuantity foreach (var record in lockRecords) { record.AllocatedQuantity += actualOutboundQuantity; } // 批量更新 _outboundLockInfoRepository.UpdateData(lockRecords); } } /// /// 检查出库单是否完成 /// public bool CheckOutboundOrderCompleted(string orderNo) { Dt_OutboundOrder outboundOrder = _outboundRepository.QueryFirst(x => x.OrderNo == orderNo); if (outboundOrder == null) return false; List details = _detailRepository.QueryData(x => x.OrderId == outboundOrder.Id); // 检查所有明细的已出数量是否都等于单据数量 return details.All(x => x.OverOutQuantity >= x.OrderQuantity - x.MoveQty); } } }