using System.Reflection.Emit; using AutoMapper; using Dm.filter; using MailKit.Search; using Mapster; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Org.BouncyCastle.Asn1.Ocsp; using Org.BouncyCastle.Crypto; using SqlSugar; using WIDESEA_BasicService; 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.CodeConfigEnum; using WIDESEA_Core.Helper; using WIDESEA_DTO.Basic; using WIDESEA_DTO.CalcOut; using WIDESEA_DTO.ReturnMES; using WIDESEA_IBasicService; using WIDESEA_IOutboundService; using WIDESEA_IRecordService; using WIDESEA_IStockService; using WIDESEA_Model.Models; using static HslCommunication.Profinet.Knx.KnxCode; namespace WIDESEA_OutboundService { public partial class OutboundService : IOutboundService { private readonly IMapper _mapper; 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; private readonly IFeedbackMesService _feedbackMesService; private readonly IRepository _taskRepository; private readonly ILocationInfoService _locationInfoService; private readonly IESSApiService _eSSApiService; private readonly IRepository _allocateOrderRepository; private readonly IRepository _allocateMaterialInfoRepository; 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 OutboundService(IMapper mapper, IUnitOfWorkManage unitOfWorkManage, IRepository detailRepository, IRepository outboundRepository, IRepository outboundLockInfoRepository, IRepository stockInfoRepository, IRepository stockDetailRepository, IRepository stockChangeRepository, IRepository stockDetailHistoryRepository, IBasicService basicService, IOutboundOrderDetailService outboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutStockLockInfoService outboundStockLockInfoService, IFeedbackMesService feedbackMesService, IRepository taskRepository, ILocationInfoService locationInfoService, IESSApiService eSSApiService, IRepository allocateOrderRepository, IRepository allocateMaterialInfoRepository) { _mapper = mapper; _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; _feedbackMesService = feedbackMesService; _taskRepository = taskRepository; _locationInfoService = locationInfoService; _eSSApiService = eSSApiService; _allocateOrderRepository = allocateOrderRepository; _allocateMaterialInfoRepository = allocateMaterialInfoRepository; } #region 出库分配 /// /// 分拣出库操作 /// /// 分拣出库请求 /// 分拣出库响应 public WebResponseContent ProcessPickingOutbound(PickingOutboundRequestDTO request) { WebResponseContent content = WebResponseContent.Instance; PickingOutboundResponseDTO response = new PickingOutboundResponseDTO(); try { _unitOfWorkManage.BeginTran(); // 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); decimal allallocatedQuantity = Math.Min(materielCalc.UnallocatedQuantity, materielPickedDetails.PickedDetails.Sum(x => x.OutboundQuantity)); materielCalc.UnallocatedQuantity = allallocatedQuantity; // 更新出库单明细(增加锁定数量,不增加已出数量) foreach (var detail in materielCalc.Details) { if (allallocatedQuantity <= 0) break; decimal lockQuantity = (detail.OrderQuantity - detail.OverOutQuantity); if (lockQuantity < materielCalc.UnallocatedQuantity) { detail.LockQuantity += lockQuantity; // 增加锁定数量 不更新 OverOutQuantity 和 OrderDetailStatus,因为还没有实际出库 outboundOrderDetails.Add(detail); materielCalc.UnallocatedQuantity -= lockQuantity; } 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); // 8. 添加任务数据 _taskRepository.AddData(tasks); _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 = new List(); if (request.DetailIds == null || !request.DetailIds.Any()) { selectedDetails = _detailRepository.QueryData(x => x.OrderId == outboundOrder.Id); } else { // 获取选择的出库明细 selectedDetails = _detailRepository.QueryData(x => x.OrderId == outboundOrder.Id && request.DetailIds.Contains(x.Id)); } //if (outboundOrder.IsBatch == 1 && request.DetailIds.Count == 1) //{ // selectedDetails = _detailRepository.QueryData(x => x.OrderId == selectedDetails.First().OrderId && 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 || request.DetailIds.Count != 1) { // 多明细出库:按物料分组处理 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; } decimal inputQuantity = request.OutboundQuantity.Value; List outboundOrderDetails = new List(); foreach (var item in selectedDetails) { inputQuantity -= (item.OrderQuantity - item.MoveQty - item.LockQuantity); outboundOrderDetails.Add(item); if (inputQuantity <= 0) { break; } } 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 = outboundOrderDetails } }; outboundOrder.Details = outboundOrderDetails; } 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.BatchNo, x.SupplyCode, x.WarehouseCode }) .Select(g => new MaterielOutboundCalculationDTO { MaterielCode = g.Key.MaterielCode, 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 = Math.Min(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; } } #endregion #region 整箱出库 public WebResponseContent CompleteOutboundWithPallet(OutboundCompletePalletRequestDTO request) { WebResponseContent content = WebResponseContent.Instance; OutboundCompleteResponseDTO response = new(); try { // 1. 根据托盘号查找库存信息 Dt_StockInfo stockInfo = _stockInfoRepository.Db.Queryable().Where(x => x.PalletCode == request.PalletCode).Includes(x => x.Details).First(); if (stockInfo == null) { response.Success = false; response.Message = $"托盘号 {request.PalletCode} 对应的库存不存在"; return WebResponseContent.Instance.Error(response.Message); } if (!stockInfo.Details.Any()) { response.Success = false; response.Message = $"托盘 {request.PalletCode} 对应的库存明细不存在"; return WebResponseContent.Instance.Error(response.Message); } bool isMatMixed = stockInfo.Details.GroupBy(x => new { x.MaterielCode, x.MaterielName, x.BatchNo, x.SupplyCode, x.WarehouseCode }).Count() > 1; if (isMatMixed) { response.Success = false; response.Message = $"混料托盘 {request.PalletCode} 不能整箱出库"; return WebResponseContent.Instance.Error(response.Message); } // 2. 查找出库单信息 Dt_OutboundOrder outboundOrder = _outboundRepository.QueryFirst(o => o.OrderNo == request.OrderNo); if (outboundOrder == null) { response.Success = false; response.Message = $"出库单 {request.OrderNo} 不存在"; return WebResponseContent.Instance.Error(response.Message); } Dt_StockInfoDetail stockInfoDetail = stockInfo.Details.First(); // 3. 查找锁定记录 Dt_OutStockLockInfo lockInfo = _outboundLockInfoRepository.QueryFirst(x => x.OrderNo == request.OrderNo && x.StockId == stockInfo.Id && x.MaterielCode == stockInfoDetail.MaterielCode && x.PalletCode == stockInfo.PalletCode); if (lockInfo == null || lockInfo.AssignQuantity <= 0) { response.Success = false; response.Message = $"该库存没有分配出库量,托盘号:{request.PalletCode}"; return WebResponseContent.Instance.Error(response.Message); } // 找出已分配的订单明细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); } } // 4. 查找出库单明细信息 List outboundOrderDetails = FindMatchingOutboundDetails(outboundOrder.Id, stockInfoDetail, detailIds); if (!outboundOrderDetails.Any()) { response.Success = false; response.Message = $"未找到匹配的出库单明细,物料:{stockInfoDetail.MaterielCode},批次:{stockInfoDetail.BatchNo}"; return WebResponseContent.Instance.Error(response.Message); } decimal totalStockQuantity = stockInfo.Details.Sum(x => x.StockQuantity); // 5. 计算实际出库量 decimal actualOutboundQuantity = CalculateActualOutboundQuantity(stockInfo.Details, 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.PalletCode},库存量:{totalStockQuantity},已出库:{totalAllocatedQuantity},分配量:{lockInfo.AssignQuantity},明细剩余:{detailRemainingQuantity}"; return WebResponseContent.Instance.Error(response.Message); } if (lockInfo.AssignQuantity != totalStockQuantity) { response.Success = false; response.Message = $"无法出库,托盘号:{request.PalletCode},库存量:{totalStockQuantity},分配量:{lockInfo.AssignQuantity}"; return WebResponseContent.Instance.Error(response.Message); } // 6. 开启事务 _unitOfWorkManage.BeginTran(); try { // 整箱出库无需拆包 PerformFullOutboundOperation(stockInfo, request, lockInfo.TaskNum.GetValueOrDefault()); if (outboundOrder.OrderType != 0) { Dt_AllocateOrder allocateOrder = _allocateOrderRepository.QueryFirst(x => x.OrderNo == outboundOrder.OrderNo); if (allocateOrder != null) { List allocateMaterialInfos = new List(); foreach (var item in stockInfo.Details) { Dt_AllocateMaterialInfo allocateMaterialInfo = new Dt_AllocateMaterialInfo() { Barcode = item.Barcode, BatchNo = item.BatchNo, FactoryArea = item.FactoryArea, MaterialCode = item.MaterielCode, MaterialName = item.MaterielName, OrderId = outboundOrder.Id, OrderNo = outboundOrder.OrderNo, Quantity = item.StockQuantity, SupplyCode = item.SupplyCode, Unit = item.Unit, WarehouseCode = allocateOrder.ToWarehouse }; allocateMaterialInfos.Add(allocateMaterialInfo); } _allocateMaterialInfoRepository.AddData(allocateMaterialInfos); } } 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; //} List barcodesList = new List(); List stockInfoDetails = stockInfo.Details.Where((x => x.StockQuantity > x.OutboundQuantity)).ToList(); decimal itemQuantity = item.LockQuantity - item.OverOutQuantity; foreach (var stockDetail in stockInfoDetails) { if (itemQuantity >= stockDetail.StockQuantity - stockDetail.OutboundQuantity) { Barcodes barcodes = new Barcodes { Barcode = stockDetail.Barcode, Qty = stockDetail.StockQuantity - stockDetail.OutboundQuantity, SupplyCode = stockDetail?.SupplyCode ?? "", BatchNo = stockDetail?.BatchNo ?? "", Unit = stockDetail?.Unit ?? "" }; itemQuantity -= (stockDetail.StockQuantity - stockDetail.OutboundQuantity); stockDetail.OutboundQuantity = stockDetail.StockQuantity; barcodesList.Add(barcodes); if (itemQuantity <= 0) break; } else { Barcodes barcodes = new Barcodes { Barcode = stockDetail.Barcode, Qty = itemQuantity, SupplyCode = stockDetail?.SupplyCode ?? "", BatchNo = stockDetail?.BatchNo ?? "", Unit = stockDetail?.Unit ?? "" }; stockDetail.OutboundQuantity += itemQuantity; barcodesList.Add(barcodes); break; } } decimal barcodeQuantity = allocatedQuantity; if (item.LockQuantity - item.OverOutQuantity >= allocatedQuantity) { item.OverOutQuantity += allocatedQuantity; item.CurrentDeliveryQty += allocatedQuantity; allocatedQuantity = 0; } else { barcodeQuantity = item.LockQuantity - item.OverOutQuantity; allocatedQuantity -= (item.LockQuantity - item.OverOutQuantity); item.OverOutQuantity = item.LockQuantity; item.CurrentDeliveryQty = item.LockQuantity; } updateDetails.Add(item); if (!string.IsNullOrEmpty(item.ReturnJsonData)) { barcodesList.AddRange(JsonConvert.DeserializeObject>(item.ReturnJsonData) ?? new List()); } 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(); response.Success = true; response.Message = "出库完成"; response.UpdatedDetails = updateDetails; // 检查出库单是否完成 if (CheckOutboundOrderCompleted(request.OrderNo)) { UpdateOutboundOrderStatus(request.OrderNo, OutOrderStatusEnum.出库完成.ObjToInt()); if (outboundOrder.OrderType != OutOrderTypeEnum.InternalAllocat.ObjToInt()) { _feedbackMesService.OutboundFeedback(outboundOrder.OrderNo); } } } 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 decimal CalculateActualOutboundQuantity(List stockDetails, 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), stockDetails.Sum(x => x.StockQuantity)); } /// /// 执行完整出库操作(不拆包) /// private void PerformFullOutboundOperation(Dt_StockInfo stockInfo, OutboundCompletePalletRequestDTO request, int taskNum) { List historyRecords = new List(); List changeRecords = new List(); foreach (var item in stockInfo.Details) { // 保存库存明细到历史记录 Dt_StockInfoDetail_Hty historyRecord = new Dt_StockInfoDetail_Hty { SourceId = item.Id, OperateType = "出库完成", InsertTime = DateTime.Now, StockId = item.StockId, MaterielCode = item.MaterielCode, MaterielName = item.MaterielName, OrderNo = item.OrderNo, BatchNo = item.BatchNo, ProductionDate = item.ProductionDate, EffectiveDate = item.EffectiveDate, SerialNumber = item.SerialNumber, StockQuantity = item.StockQuantity, OutboundQuantity = item.StockQuantity, Status = item.Status, Unit = item.Unit, InboundOrderRowNo = item.InboundOrderRowNo, SupplyCode = item.SupplyCode, FactoryArea = item.FactoryArea, WarehouseCode = item.WarehouseCode, Barcode = item.Barcode, CreateDate = item.CreateDate, Creater = item.Creater, Remark = $"整箱出库完成删除,条码:{request.PalletCode},原数量:{item.StockQuantity},出库数量:{item.StockQuantity},操作者:{request.Operator}" }; historyRecords.Add(historyRecord); // 记录库存变动 Dt_StockQuantityChangeRecord changeRecord = new Dt_StockQuantityChangeRecord { StockDetailId = item.Id, PalleCode = stockInfo.PalletCode, MaterielCode = item.MaterielCode, MaterielName = item.MaterielName, BatchNo = item.BatchNo, OriginalSerilNumber = item.Barcode, NewSerilNumber = "", OrderNo = request.OrderNo, TaskNum = taskNum, ChangeType = (int)StockChangeTypeEnum.Outbound, ChangeQuantity = -item.StockQuantity, BeforeQuantity = item.StockQuantity, AfterQuantity = 0, SupplyCode = item.SupplyCode, WarehouseCode = item.WarehouseCode, Remark = $"整箱出库完成删除库存明细,条码:{request.PalletCode},出库数量:{item.StockQuantity},操作者:{request.Operator}" }; changeRecords.Add(changeRecord); } _stockDetailHistoryRepository.AddData(historyRecords); // 删除库存明细记录 _stockDetailRepository.DeleteData(stockInfo.Details); _stockChangeRepository.AddData(changeRecords); } #endregion #region 拣选 /// /// 出库完成处理(扫描条码扣减库存) /// /// 出库完成请求 /// 出库完成响应 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; List returnDTOs = new List(); string newBarcode = string.Empty; // 9. 开启事务 _unitOfWorkManage.BeginTran(); try { decimal beforeQuantity = stockDetail.StockQuantity; // 原始库存量 Dt_AllocateMaterialInfo allocateMaterialInfo = new Dt_AllocateMaterialInfo(); // 根据是否拆包执行不同的操作 if (isUnpacked) { (string NewBarcode, List MaterialCodeReturnDTOs) result = PerformUnpackOperation(stockDetail, stockInfo, actualOutboundQuantity, request, beforeQuantity, lockInfo.TaskNum.GetValueOrDefault(), outboundOrder.Id, outboundOrder.OrderNo); returnDTOs = result.MaterialCodeReturnDTOs; newBarcode = result.NewBarcode; MaterialCodeReturnDTO returnDTO = returnDTOs.First(x => x.Barcode == newBarcode); if (outboundOrder.OrderType != 0) { allocateMaterialInfo = new Dt_AllocateMaterialInfo() { Barcode = returnDTO.Barcode, BatchNo = returnDTO.BatchNo, FactoryArea = returnDTO.FactoryArea, MaterialCode = returnDTO.MaterialCode, MaterialName = returnDTO.MaterialName, OrderId = outboundOrder.Id, OrderNo = outboundOrder.OrderNo, Quantity = returnDTO.Quantity, SupplyCode = returnDTO.SuplierCode, Unit = stockDetail.Unit }; } } else { PerformFullOutboundOperation(stockDetail, stockInfo, actualOutboundQuantity, request, beforeQuantity, lockInfo.TaskNum.GetValueOrDefault()); if (outboundOrder.OrderType != 0) { allocateMaterialInfo = new Dt_AllocateMaterialInfo() { Barcode = stockDetail.Barcode, BatchNo = stockDetail.BatchNo, FactoryArea = stockDetail.FactoryArea, MaterialCode = stockDetail.MaterielCode, MaterialName = stockDetail.MaterielName, OrderId = outboundOrder.Id, OrderNo = outboundOrder.OrderNo, Quantity = stockDetail.StockQuantity, SupplyCode = stockDetail.SupplyCode, Unit = stockDetail.Unit }; } } // 判断是否是调拨单 if (outboundOrder.OrderType != 0) { Dt_AllocateOrder allocateOrder = _allocateOrderRepository.QueryFirst(x => x.OrderNo == outboundOrder.OrderNo); if (allocateOrder != null) { allocateMaterialInfo.WarehouseCode = allocateOrder.ToWarehouse; _allocateMaterialInfoRepository.AddData(allocateMaterialInfo); } } 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; //} decimal barcodeQuantity = allocatedQuantity; if (item.LockQuantity - item.OverOutQuantity >= allocatedQuantity) { item.OverOutQuantity += allocatedQuantity; item.CurrentDeliveryQty += allocatedQuantity; allocatedQuantity = 0; } else { barcodeQuantity = item.LockQuantity - item.OverOutQuantity; 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 = newBarcode, Qty = barcodeQuantity, 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, MaterialCodes = returnDTOs }; response.Success = true; response.Message = "出库完成"; response.ScannedDetail = scannedDetail; response.UpdatedDetails = updateDetails; if (!string.IsNullOrEmpty(newBarcode)) { // 物料新条码回传 _feedbackMesService.BarcodeFeedback(newBarcode); } // 检查出库单是否完成 if (CheckOutboundOrderCompleted(request.OrderNo)) { UpdateOutboundOrderStatus(request.OrderNo, OutOrderStatusEnum.出库完成.ObjToInt()); if (outboundOrder.OrderType != OutOrderTypeEnum.InternalAllocat.ObjToInt()) { _feedbackMesService.OutboundFeedback(outboundOrder.OrderNo); } } } 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 NewBarcode, List MaterialCodeReturnDTOs) PerformUnpackOperation(Dt_StockInfoDetail stockDetail, Dt_StockInfo stockInfo, decimal actualOutboundQuantity, OutboundCompleteRequestDTO request, decimal beforeQuantity, int taskNum, int orderId, string orderNo) { string newBarcode = GenerateNewBarcode(); string remark = $"拆包记录,原条码:{request.Barcode},原数量:{stockDetail.StockQuantity},出库条码:{newBarcode}, 出库数量:{actualOutboundQuantity},回库条码:{request.Barcode},回库数量:{stockDetail.StockQuantity - actualOutboundQuantity},操作者:{request.Operator}"; List materialCodeInfos = CreateMaterialCodeInfos(stockDetail, newBarcode, actualOutboundQuantity, remark, taskNum, orderId, orderNo); List returnDTOs = _mapper.Map>(materialCodeInfos); // 保存原始库存明细到历史记录 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, Creater = stockDetail.Creater, CreateDate = stockDetail.CreateDate, FactoryArea = stockDetail.FactoryArea, WarehouseCode = stockDetail.WarehouseCode, Remark = $"拆包前原始记录,出库单号:{orderNo},出库单主键:{orderId},原条码:{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 = $"拆包后更新,出库单号:{orderNo},出库单主键:{orderId},原条码:{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 = $"拆包出库,出库单号:{orderNo},出库单主键:{orderId},原条码:{request.Barcode},新条码:{newBarcode},出库数量:{actualOutboundQuantity},剩余:{remainingQuantity},操作者:{request.Operator}" }; _stockChangeRepository.AddData(unpackChangeRecord); return (newBarcode, returnDTOs); } /// /// 执行完整出库操作(不拆包) /// 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, Creater = stockDetail.Creater, CreateDate = stockDetail.CreateDate, 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; } /// /// /// /// /// /// /// /// /// private List CreateMaterialCodeInfos(Dt_StockInfoDetail stockDetail, string newBarcode, decimal splitQuantity, string remark, int taskNum, int orderId, string orderNo) { List materialCodeInfos = new List(); Dt_MaterielInfo? materielInfo = _basicService.MaterielInfoService.Repository.QueryFirst(x => x.MaterielCode == stockDetail.MaterielCode); Dt_MaterialCodeInfo outMaterialCodeInfo = new Dt_MaterialCodeInfo() { AfterQuantity = splitQuantity, BatchNo = stockDetail.BatchNo, FactoryArea = stockDetail.FactoryArea, MaterialName = materielInfo?.MaterielName ?? stockDetail.MaterielName, MaterialSpec = materielInfo?.MaterielSpec ?? "", MaterialCode = stockDetail.MaterielCode, NewBarcode = newBarcode, OldBarcode = stockDetail.Barcode, OriginalQuantity = stockDetail.StockQuantity, PruchaseOrderNo = stockDetail.OrderNo, SuplierCode = stockDetail.SupplyCode, Unit = stockDetail.Unit, Date = DateTime.Now.ToString("yyyy-MM-dd"), Remark = remark, WarehouseCode = stockDetail.WarehouseCode, OrderNo = orderNo, OrderId = orderId, ReturnStatus = 0 }; materialCodeInfos.Add(outMaterialCodeInfo); Dt_MaterialCodeInfo returnMaterialCodeInfo = new Dt_MaterialCodeInfo() { AfterQuantity = stockDetail.StockQuantity - splitQuantity, BatchNo = stockDetail.BatchNo, FactoryArea = stockDetail.FactoryArea, MaterialName = materielInfo?.MaterielName ?? stockDetail.MaterielName, MaterialSpec = materielInfo?.MaterielSpec ?? "", MaterialCode = stockDetail.MaterielCode, NewBarcode = stockDetail.Barcode, OldBarcode = stockDetail.Barcode, OriginalQuantity = stockDetail.StockQuantity, PruchaseOrderNo = stockDetail.OrderNo, SuplierCode = stockDetail.SupplyCode, Unit = stockDetail.Unit, Date = DateTime.Now.ToString("yyyy-MM-dd"), Remark = remark, WarehouseCode = stockDetail.WarehouseCode, OrderNo = orderNo, OrderId = orderId, ReturnStatus = 0 }; materialCodeInfos.Add(returnMaterialCodeInfo); _basicService.MaterielCodeInfoService.Repository.AddData(materialCodeInfos); return materialCodeInfos; } /// /// 更新该托盘该物料的所有锁定记录的累计已出库数量 /// /// 库存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); } #endregion #region 取空箱 public async Task EmptyBox(string palletCode) { WebResponseContent content = new WebResponseContent(); try { var stock = await _stockInfoRepository.Db.Queryable().Includes(x => x.Details).Where(x => x.PalletCode == palletCode).FirstAsync(); if (stock == null) { return content.Error($"未找到托盘{palletCode}库存信息"); } if (stock.Details.Count > 0) { return content.Error($"托盘{palletCode}还存在库存信息不允许取走"); } Dt_StockInfo_Hty stockInfo_Hty = stock.Adapt(); stockInfo_Hty.SourceId = stock.Id; stockInfo_Hty.OperateType = "取空箱"; stockInfo_Hty.InsertTime = DateTime.Now; _unitOfWorkManage.BeginTran(); await _outboundRepository.Db.InsertNav(stockInfo_Hty).IncludesAllFirstLayer().ExecuteCommandAsync(); await _stockInfoRepository.DeleteDataByIdAsync(stock.Id); _unitOfWorkManage.CommitTran(); return content.OK(); } catch (Exception ex) { _unitOfWorkManage.RollbackTran(); return content.Error(ex.Message); } } #endregion #region 回库 public async Task ReturnToWarehouse(string palletCode, string OrderNo, string station) { WebResponseContent content = new WebResponseContent(); try { var stock = await _stockInfoRepository.Db.Queryable().Includes(x => x.Details).Where(x => x.PalletCode == palletCode).FirstAsync(); if (stock == null) { return content.Error($"未找到托盘{palletCode}库存信息不允许回库"); } if (stock.Details.Count <= 0) { stock.PalletType = (int)PalletTypeEnum.Empty; stock.StockStatus = (int)StockStatusEmun.组盘暂存; stock.LocationCode = ""; } else if (stock.Details.Count > 0) { Dt_OutStockLockInfo lockInfo = _outboundLockInfoRepository.QueryFirst(x => x.OrderNo == OrderNo && x.StockId == stock.Id && x.PalletCode == palletCode); if (lockInfo != null && lockInfo.SortedQuantity != lockInfo.AssignQuantity) { return content.Error($"托盘{palletCode}库存未拣选完不允许回库"); } stock.StockStatus = (int)StockStatusEmun.组盘暂存; stock.LocationCode = ""; } var task = await _taskRepository.Db.Queryable() .Where(x => x.PalletCode == palletCode) .FirstAsync(); if (task != null) { return content.Error($"托盘{palletCode}存在任务回库失败!"); } // 分配新货位 var newLocation = _locationInfoService.AssignLocation(stock.LocationType); var newTask = new Dt_Task() { CurrentAddress = stations[station], Grade = 0, PalletCode = palletCode, NextAddress = "", OrderNo = OrderNo, Roadway = newLocation.RoadwayNo, SourceAddress = stations[station], TargetAddress = newLocation.LocationCode, TaskStatus = (int)TaskStatusEnum.New, TaskType = stock.Details.Count > 0 ? (int)TaskTypeEnum.InPick : (int)TaskTypeEnum.InEmpty, PalletType = stock.PalletType, WarehouseId = stock.WarehouseId }; _stockInfoRepository.UpdateData(stock); _taskRepository.AddData(newTask); //var moveResult = await _eSSApiService.MoveContainerAsync(new MoveContainerRequest //{ // slotCode = movestations[station], // containerCode = palletCode //}); return content.OK(); } catch (Exception ex) { return content.Error(ex.Message); } } #endregion } }