feat: SKU/入库预报返回客户信息,库存出入库历史与可用冻结量
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f4ddd993b9
commit
fd9f57bac1
@ -24,6 +24,10 @@ public class WarehouseSkuOutput
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string? CustomerName { get; set; }
|
||||
|
||||
public string? CustomerCode { get; set; }
|
||||
|
||||
public string SkuCode { get; set; } = string.Empty;
|
||||
|
||||
public string SkuName { get; set; } = string.Empty;
|
||||
@ -166,6 +170,10 @@ public class WarehouseInboundOutput
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string? CustomerName { get; set; }
|
||||
|
||||
public string? CustomerCode { get; set; }
|
||||
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
public string NoticeNo { get; set; } = string.Empty;
|
||||
@ -389,6 +397,15 @@ public class WarehouseParcelOutput
|
||||
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>商品总数量。</summary>
|
||||
public int TotalQty { get; set; }
|
||||
|
||||
/// <summary>已冻结数量(订单绑定占用)。</summary>
|
||||
public int FrozenQty { get; set; }
|
||||
|
||||
/// <summary>可用数量 = 总量 - 冻结。</summary>
|
||||
public int AvailableQty { get; set; }
|
||||
|
||||
public List<WarehouseParcelItemOutput> Items { get; set; } = [];
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
@ -505,6 +522,36 @@ public class WarehouseInventoryRelocateInput
|
||||
public int Qty { get; set; }
|
||||
}
|
||||
|
||||
public class WarehouseInventoryHistoryInput : WarehousePageInput
|
||||
{
|
||||
[Required]
|
||||
public int SkuId { get; set; }
|
||||
|
||||
[Required]
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 流水类型:all / inbound / deduct;空视为 all。
|
||||
/// </summary>
|
||||
public string? MovementType { get; set; }
|
||||
}
|
||||
|
||||
public class WarehouseInventoryHistoryOutput
|
||||
{
|
||||
/// <summary>inbound 入库 / deduct 扣减。</summary>
|
||||
public string MovementType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>变动数量(入库为正,扣减为正数,前端按类型展示符号)。</summary>
|
||||
public int Qty { get; set; }
|
||||
|
||||
/// <summary>关联单号:入库预报号 / 物流单号。</summary>
|
||||
public string? RefNo { get; set; }
|
||||
|
||||
public string? Remark { get; set; }
|
||||
|
||||
public DateTime OccurredAt { get; set; }
|
||||
}
|
||||
|
||||
// --- Parcel arrival(快递到仓)---
|
||||
|
||||
public class WarehouseParcelArrivalListInput : WarehousePageInput
|
||||
|
||||
@ -58,9 +58,25 @@ public partial class WarehouseService
|
||||
? []
|
||||
: await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId));
|
||||
|
||||
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
|
||||
var customers = customerIds.Length == 0
|
||||
? []
|
||||
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
||||
var customerMap = customers.ToDictionary(x => x.Id);
|
||||
|
||||
return new OmsPageResult<WarehouseInboundOutput>
|
||||
{
|
||||
Items = entities.Select(x => MapInboundOutput(x, boxes, lines)).ToList(),
|
||||
Items = entities.Select(x =>
|
||||
{
|
||||
var output = MapInboundOutput(x, boxes, lines);
|
||||
if (customerMap.TryGetValue(x.CustomerId, out var customer))
|
||||
{
|
||||
output.CustomerName = customer.CustomerName;
|
||||
output.CustomerCode = customer.CustomerCode;
|
||||
}
|
||||
|
||||
return output;
|
||||
}).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,11 +3,19 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Warehouse;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Warehouse;
|
||||
|
||||
public partial class WarehouseService
|
||||
{
|
||||
private IOrderStockBindRepository OrderStockBindRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IOrderStockBindRepository>();
|
||||
|
||||
private IOrderRepository OrderRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询库存列表。
|
||||
/// </summary>
|
||||
@ -20,9 +28,10 @@ public partial class WarehouseService
|
||||
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
|
||||
var hasWarehouseFilter = input.WarehouseId.HasValue;
|
||||
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
|
||||
// 有总量即展示(含全部冻结、可用为 0 的行),避免客户看不到被占用库存
|
||||
var inventories = await InventoryRepository.GetListAsync(x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& x.AvailableQty > 0
|
||||
&& x.Qty > 0
|
||||
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue));
|
||||
|
||||
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray();
|
||||
@ -165,4 +174,137 @@ public partial class WarehouseService
|
||||
await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.SourceBinLocationId, -input.Qty);
|
||||
await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.TargetBinLocationId, input.Qty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询 SKU 库存变动历史:入库收货 + 订单扣减。
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/inventory/history")]
|
||||
public async Task<OmsPageResult<WarehouseInventoryHistoryOutput>> GetInventoryHistoryAsync(
|
||||
WarehouseInventoryHistoryInput input)
|
||||
{
|
||||
if (input.SkuId <= 0 || input.WarehouseId <= 0)
|
||||
{
|
||||
throw new BusinessException("SKU 和仓库不能为空");
|
||||
}
|
||||
|
||||
var customerId = await ResolveListCustomerIdAsync(input.CustomerId);
|
||||
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
|
||||
await GetSkuOrThrowAsync(customerId, input.SkuId);
|
||||
|
||||
var movementType = (input.MovementType ?? "all").Trim().ToLowerInvariant();
|
||||
if (movementType is not ("all" or "inbound" or "deduct"))
|
||||
{
|
||||
movementType = "all";
|
||||
}
|
||||
|
||||
var rows = new List<WarehouseInventoryHistoryOutput>();
|
||||
|
||||
if (movementType is "all" or "inbound")
|
||||
{
|
||||
var notices = await InboundNoticeRepository.GetListAsync(x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& x.WarehouseId == input.WarehouseId);
|
||||
var noticeMap = notices.ToDictionary(x => x.Id);
|
||||
var noticeIds = notices.Select(x => x.Id).ToArray();
|
||||
var boxes = noticeIds.Length == 0
|
||||
? []
|
||||
: await InboundBoxRepository.GetListAsync(x => noticeIds.Contains(x.InboundNoticeId));
|
||||
var boxMap = boxes.ToDictionary(x => x.Id);
|
||||
var boxIds = boxes.Select(x => x.Id).ToArray();
|
||||
var lines = boxIds.Length == 0
|
||||
? []
|
||||
: await InboundBoxLineRepository.GetListAsync(x =>
|
||||
boxIds.Contains(x.InboundBoxId)
|
||||
&& x.SkuId == input.SkuId
|
||||
&& x.ReceivedQty != null
|
||||
&& x.ReceivedQty > 0);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (!boxMap.TryGetValue(line.InboundBoxId, out var box))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
noticeMap.TryGetValue(box.InboundNoticeId, out var notice);
|
||||
var occurredAt = line.UpdateTime != default ? line.UpdateTime : line.CreateTime;
|
||||
if (notice?.ActualArrivalTime != null)
|
||||
{
|
||||
occurredAt = notice.ActualArrivalTime.Value;
|
||||
}
|
||||
|
||||
rows.Add(new WarehouseInventoryHistoryOutput
|
||||
{
|
||||
MovementType = "inbound",
|
||||
Qty = line.ReceivedQty.GetValueOrDefault(),
|
||||
RefNo = notice?.NoticeNo,
|
||||
Remark = string.IsNullOrWhiteSpace(box.MarkBoxNo)
|
||||
? null
|
||||
: $"箱 {box.MarkBoxNo}",
|
||||
OccurredAt = occurredAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (movementType is "all" or "deduct")
|
||||
{
|
||||
var binds = await OrderStockBindRepository.GetListAsync(x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& x.WarehouseId == input.WarehouseId
|
||||
&& x.SkuId == input.SkuId
|
||||
&& x.BindType == OrderStockBindTypeCodes.Inventory
|
||||
&& x.Status == OrderStockBindStatusCodes.Deducted
|
||||
&& x.BindQty > 0);
|
||||
|
||||
var orderIds = binds.Select(x => x.OrderId).Distinct().ToArray();
|
||||
var orders = orderIds.Length == 0
|
||||
? []
|
||||
: await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
|
||||
var orderMap = orders.ToDictionary(x => x.Id);
|
||||
|
||||
foreach (var bind in binds)
|
||||
{
|
||||
orderMap.TryGetValue(bind.OrderId, out var order);
|
||||
var refNo = order?.ServiceNo;
|
||||
if (string.IsNullOrWhiteSpace(refNo))
|
||||
{
|
||||
refNo = order?.TrackNo;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refNo))
|
||||
{
|
||||
refNo = order?.ClientNo;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refNo))
|
||||
{
|
||||
refNo = $"#{bind.OrderId}";
|
||||
}
|
||||
|
||||
rows.Add(new WarehouseInventoryHistoryOutput
|
||||
{
|
||||
MovementType = "deduct",
|
||||
Qty = bind.BindQty,
|
||||
RefNo = refNo,
|
||||
Remark = string.IsNullOrWhiteSpace(bind.Remark) ? "订单出库扣减" : bind.Remark,
|
||||
OccurredAt = bind.UpdateTime != default ? bind.UpdateTime : bind.CreateTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var ordered = rows
|
||||
.OrderByDescending(x => x.OccurredAt)
|
||||
.ThenByDescending(x => x.RefNo)
|
||||
.ToList();
|
||||
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
var skip = (pageIndex - 1) * pageSize;
|
||||
|
||||
return new OmsPageResult<WarehouseInventoryHistoryOutput>
|
||||
{
|
||||
Items = ordered.Skip(skip).Take(pageSize).ToList(),
|
||||
Total = ordered.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -308,8 +308,25 @@ public partial class WarehouseService
|
||||
}
|
||||
}
|
||||
|
||||
private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable<CParcelItem> items) =>
|
||||
new()
|
||||
private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable<CParcelItem> items)
|
||||
{
|
||||
var itemList = items.ToList();
|
||||
var mappedItems = itemList.Select(item => new WarehouseParcelItemOutput
|
||||
{
|
||||
Id = item.Id,
|
||||
ProductName = item.ProductName,
|
||||
AttributeText = item.AttributeText,
|
||||
ImageUrl = item.ImageUrl,
|
||||
Quantity = item.Quantity,
|
||||
FrozenQty = item.FrozenQty,
|
||||
AvailableQty = item.AvailableQty > 0 ? item.AvailableQty : Math.Max(0, item.Quantity - item.FrozenQty),
|
||||
SkuId = item.SkuId,
|
||||
SkuCode = item.SkuCode,
|
||||
BinLocationId = item.BinLocationId,
|
||||
Remark = item.Remark
|
||||
}).ToList();
|
||||
|
||||
return new WarehouseParcelOutput
|
||||
{
|
||||
Id = parcel.Id,
|
||||
CustomerId = parcel.CustomerId,
|
||||
@ -320,19 +337,10 @@ public partial class WarehouseService
|
||||
ArrivalTime = parcel.ArrivalTime,
|
||||
Remark = parcel.Remark,
|
||||
CreateTime = parcel.CreateTime,
|
||||
Items = items.Select(item => new WarehouseParcelItemOutput
|
||||
{
|
||||
Id = item.Id,
|
||||
ProductName = item.ProductName,
|
||||
AttributeText = item.AttributeText,
|
||||
ImageUrl = item.ImageUrl,
|
||||
Quantity = item.Quantity,
|
||||
FrozenQty = item.FrozenQty,
|
||||
AvailableQty = item.AvailableQty > 0 ? item.AvailableQty : Math.Max(0, item.Quantity - item.FrozenQty),
|
||||
SkuId = item.SkuId,
|
||||
SkuCode = item.SkuCode,
|
||||
BinLocationId = item.BinLocationId,
|
||||
Remark = item.Remark
|
||||
}).ToList()
|
||||
TotalQty = mappedItems.Sum(x => x.Quantity),
|
||||
FrozenQty = mappedItems.Sum(x => x.FrozenQty),
|
||||
AvailableQty = mappedItems.Sum(x => x.AvailableQty),
|
||||
Items = mappedItems
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,9 +49,25 @@ public partial class WarehouseService
|
||||
.GroupBy(x => x.SkuId)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(x => x.Qty));
|
||||
|
||||
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
|
||||
var customers = customerIds.Length == 0
|
||||
? []
|
||||
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
||||
var customerMap = customers.ToDictionary(x => x.Id);
|
||||
|
||||
return new OmsPageResult<WarehouseSkuOutput>
|
||||
{
|
||||
Items = entities.Select(x => MapSkuOutput(x, qtyMap.GetValueOrDefault(x.Id))).ToList(),
|
||||
Items = entities.Select(x =>
|
||||
{
|
||||
var output = MapSkuOutput(x, qtyMap.GetValueOrDefault(x.Id));
|
||||
if (customerMap.TryGetValue(x.CustomerId, out var customer))
|
||||
{
|
||||
output.CustomerName = customer.CustomerName;
|
||||
output.CustomerCode = customer.CustomerCode;
|
||||
}
|
||||
|
||||
return output;
|
||||
}).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user