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.Helper;
using WIDESEA_DTO.CalcOut;
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 IOutboundOrderDetailService _detailService;
private readonly IOutboundOrderService _outboundOrderService;
private readonly IOutStockLockInfoService _outboundLockInfoService;
private readonly IStockInfoService _stockInfoService;
private readonly IStockInfoDetailService _stockDetailService;
private readonly ILocationInfoService _locationInfoService;
private readonly IStockQuantityChangeRecordService _stockChangeService;
private readonly IStockInfoDetail_HtyService _stockDetailHistoryService;
public OutboundService(IUnitOfWorkManage unitOfWorkManage, IOutboundOrderDetailService outboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutStockLockInfoService outboundStockLockInfoService, IStockInfoService stockInfoService, IStockInfoDetailService stockDetailService, ILocationInfoService locationInfoService, IStockQuantityChangeRecordService stockQuantityChangeRecordService, IStockInfoDetail_HtyService stockDetailHistoryService)
{
_unitOfWorkManage = unitOfWorkManage;
Db = _unitOfWorkManage.GetDbClient();
OutboundOrderDetailService = outboundOrderDetailService;
OutboundOrderService = outboundOrderService;
OutboundStockLockInfoService = outboundStockLockInfoService;
_detailService = outboundOrderDetailService;
_outboundOrderService = outboundOrderService;
_outboundLockInfoService = outboundStockLockInfoService;
_stockInfoService = stockInfoService;
_stockDetailService = stockDetailService;
_locationInfoService = locationInfoService;
_stockChangeService = stockQuantityChangeRecordService;
_stockDetailHistoryService = stockDetailHistoryService;
}
///
/// 分拣出库操作
///
/// 分拣出库请求
/// 分拣出库响应
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;
}
else
{
materielCalc.OutStockLockInfos.Add(item);
}
outStockLockInfos.Add(item);
}
tasks.AddRange(materielPickedDetails.Tasks);
pickedDetails.AddRange(materielPickedDetails.PickedDetails);
// 更新出库单明细(增加锁定数量,不增加已出数量)
foreach (var detail in materielCalc.Details)
{
decimal lockQuantity = (detail.OrderQuantity - detail.OverOutQuantity);
detail.LockQuantity += lockQuantity; // 增加锁定数量 不更新 OverOutQuantity 和 OrderDetailStatus,因为还没有实际出库
outboundOrderDetails.Add(detail);
}
}
// 3. 更新出库单状态为出库中(表示已有任务分配)
UpdateOutboundOrderStatus(request.OrderNo, (int)OutOrderStatusEnum.出库中);
// 4. 更新出库单明细锁定数量
_detailService.Repository.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 = _outboundOrderService.Repository.QueryFirst(x => x.OrderNo == request.OrderNo);
if (outboundOrder == null)
{
result.CanOutbound = false;
result.ErrorMessage = $"出库单 {request.OrderNo} 不存在";
return result;
}
result.FactoryArea = outboundOrder.FactoryArea;
result.IsMultiDetail = request.IsMultiDetail;
// 获取选择的出库明细
List selectedDetails = _detailService.Repository.QueryData(x => x.OrderId == outboundOrder.Id && request.DetailIds.Contains(x.Id));
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 (request.IsMultiDetail)
{
// 多明细出库:按物料分组处理
result.MaterielCalculations = CalcMaterielOutboundQuantities(outboundOrder, selectedDetails.ToList());
}
else
{
// 单明细出库:验证输入的出库数量
if (!request.OutboundQuantity.HasValue || request.OutboundQuantity.Value <= 0)
{
result.CanOutbound = false;
result.ErrorMessage = "单明细出库时必须指定出库数量且大于0";
return result;
}
Dt_OutboundOrderDetail? singleDetail = selectedDetails.First();
//判断可出库数量
if (singleDetail.OrderQuantity - singleDetail.LockQuantity - singleDetail.MoveQty <= 0)
{
result.CanOutbound = false;
result.ErrorMessage = $"本次出库数量 {request.OutboundQuantity.Value} 超过可出库数量 {singleDetail.OrderQuantity - singleDetail.LockQuantity - singleDetail.MoveQty}";
return result;
}
result.MaterielCalculations = new List()
{
new MaterielOutboundCalculationDTO
{
MaterielCode = singleDetail.MaterielCode,
MaterielName = singleDetail.MaterielName,
BatchNo = singleDetail.BatchNo,
SupplyCode = singleDetail.SupplyCode,
WarehouseCode = singleDetail.WarehouseCode,
TotalOrderQuantity = singleDetail.OrderQuantity - singleDetail.MoveQty,
TotalOverOutQuantity = singleDetail.OverOutQuantity,
AssignedQuantity = singleDetail.LockQuantity,
UnallocatedQuantity = singleDetail.OrderQuantity - singleDetail.LockQuantity - singleDetail.MoveQty,
MovedQuantity = singleDetail.MoveQty,
Details = new List() { singleDetail }
}
};
}
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 = _outboundLockInfoService.Repository.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 = _stockDetailService.Repository.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 = _locationInfoService.Repository.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 = _stockInfoService.Repository.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 = _outboundLockInfoService.Repository.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 = _outboundOrderService.Repository.QueryFirst(x => x.OrderNo == orderNo);
if (outboundOrder == null) return false;
outboundOrder.OrderStatus = status;
_outboundOrderService.Repository.UpdateData(outboundOrder);
return true;
}
catch
{
return false;
}
}
///
///
///
///
///
///
public bool UpdateStockStatus(List palletCodes, int status)
{
try
{
List stockInfos = _stockInfoService.Repository.QueryData(x => palletCodes.Contains(x.PalletCode));
stockInfos.ForEach(stockInfo =>
{
stockInfo.StockStatus = status;
});
_stockInfoService.Repository.UpdateData(stockInfos);
return true;
}
catch
{
return false;
}
}
///
///
///
///
///
///
public bool UpdateLocationStatus(List locationCodes, int status)
{
try
{
List locationInfos = _locationInfoService.Repository.QueryData(x => locationCodes.Contains(x.LocationCode));
locationInfos.ForEach(x =>
{
x.LocationStatus = status;
});
_locationInfoService.Repository.UpdateData(locationInfos);
return true;
}
catch
{
return false;
}
}
///
///
///
///
///
public bool UpdateOutStockLockInfo(List outStockLockInfos)
{
try
{
List updateData = outStockLockInfos.Where(x => x.Id > 0).ToList();
_outboundLockInfoService.Repository.UpdateData(updateData);
List addData = outStockLockInfos.Where(x => x.Id <= 0).ToList();
_outboundLockInfoService.Repository.AddData(addData);
return true;
}
catch
{
return false;
}
}
///
/// 出库完成处理(扫描条码扣减库存)
///
/// 出库完成请求
/// 出库完成响应
public WebResponseContent CompleteOutboundWithBarcode(OutboundCompleteRequestDTO request)
{
WebResponseContent content = WebResponseContent.Instance;
OutboundCompleteResponseDTO response = new();
try
{
// 1. 根据托盘号查找库存信息
Dt_StockInfo stockInfo = _stockInfoService.Repository.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 = _stockDetailService.Repository.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 = _outboundOrderService.Repository.QueryFirst(o => o.OrderNo == request.OrderNo);
if (outboundOrder == null)
{
response.Success = false;
response.Message = $"出库单 {request.OrderNo} 不存在";
return WebResponseContent.Instance.Error($"出库单 {request.OrderNo} 不存在");
}
// 5. 查找出库单明细信息
List outboundOrderDetails = FindMatchingOutboundDetails(outboundOrder.Id, stockDetail);
if (!outboundOrderDetails.Any())
{
response.Success = false;
response.Message = $"未找到匹配的出库单明细,物料:{stockDetail.MaterielCode},批次:{stockDetail.BatchNo}";
return WebResponseContent.Instance.Error($"未找到匹配的出库单明细,物料:{stockDetail.MaterielCode},批次:{stockDetail.BatchNo}");
}
// 6. 查找锁定记录
Dt_OutStockLockInfo lockInfo = _outboundLockInfoService.Repository.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}");
}
// 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}");
}
// 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;
}
updateDetails.Add(item);
}
lockInfo.SortedQuantity = lockInfo.SortedQuantity + actualOutboundQuantity;
// 更新锁定记录
_outboundLockInfoService.Repository.UpdateData(lockInfo);
// 更新出库单明细的已出库数量
_detailService.Repository.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 details = _detailService.Repository.QueryData(x =>
x.OrderId == orderId &&
x.MaterielCode == stockDetail.MaterielCode && x.OrderQuantity - x.MoveQty > x.OverOutQuantity);
// 精确匹配:处理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}"
};
_stockDetailHistoryService.Repository.AddData(originalHistoryRecord);
// 保存剩余部分到历史记录
decimal remainingQuantity = stockDetail.StockQuantity - actualOutboundQuantity;
if (remainingQuantity > 0)
{
// 更新原库存明细
stockDetail.StockQuantity = remainingQuantity;
//stockDetail.Barcode = newBarcode;
stockDetail.Remark = $"拆包后更新,原条码:{request.Barcode},新数量:{remainingQuantity},操作者:{request.Operator}";
_stockDetailService.Repository.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}"
};
_stockChangeService.Repository.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}"
};
_stockDetailHistoryService.Repository.AddData(historyRecord);
// 删除库存明细记录
_stockDetailService.Repository.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}"
};
_stockChangeService.Repository.AddData(changeRecord);
}
///
/// 生成新的条码
///
/// 新条码
private string GenerateNewBarcode()
{
// 使用时间戳和随机数生成唯一条码
string newBarcode = string.Empty;
//todo 重新生成条码逻辑
return newBarcode;
}
///
/// 更新该托盘该物料的所有锁定记录的累计已出库数量
///
/// 库存ID
/// 物料编号
/// 批次号
/// 本次实际出库数量
///
private void UpdateLockInfoAllocatedQuantity(int stockId, string materielCode, string batchNo, decimal actualOutboundQuantity)
{
// 查询该托盘该物料的所有锁定记录
List lockRecords = _outboundLockInfoService.Repository.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;
}
// 批量更新
_outboundLockInfoService.Repository.UpdateData(lockRecords);
}
}
///
/// 检查出库单是否完成
///
public bool CheckOutboundOrderCompleted(string orderNo)
{
Dt_OutboundOrder outboundOrder = _outboundOrderService.Repository.QueryFirst(x => x.OrderNo == orderNo);
if (outboundOrder == null) return false;
List details = _detailService.Repository.QueryData(x => x.OrderId == outboundOrder.Id);
// 检查所有明细的已出数量是否都等于单据数量
return details.All(x => x.OverOutQuantity >= x.OrderQuantity);
}
}
}