补齐运输规划与任务服务在状态/优先级字段上的读写与调度衔接。 Co-authored-by: Cursor <cursoragent@cursor.com>
1010 lines
46 KiB
C#
1010 lines
46 KiB
C#
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using MiGu.Server.Persistence;
|
|
|
|
namespace MiGu.Server.Wms;
|
|
|
|
public sealed class WmsService
|
|
{
|
|
private readonly PlatformDbContext _db;
|
|
private readonly WmsReferenceValidator _refs;
|
|
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
|
|
|
public WmsService(PlatformDbContext db, WmsReferenceValidator refs)
|
|
{
|
|
_db = db;
|
|
_refs = refs;
|
|
}
|
|
|
|
public Task<List<Warehouse>> Warehouses(string? q = null) =>
|
|
FilterByKeyword(_db.Warehouses.AsNoTracking().OrderBy(x => x.SortOrder).ThenBy(x => x.Code), q).ToListAsync();
|
|
|
|
public Task<List<WarehouseArea>> Areas(string? q = null, Guid? warehouseId = null)
|
|
{
|
|
var query = _db.WarehouseAreas.AsNoTracking().AsQueryable();
|
|
if (warehouseId.HasValue) query = query.Where(x => x.WarehouseId == warehouseId.Value);
|
|
return FilterByKeyword(query.OrderBy(x => x.SortOrder).ThenBy(x => x.Code), q).ToListAsync();
|
|
}
|
|
|
|
public Task<List<Storage>> Storages(string? q = null, Guid? areaId = null, string? locationKind = null)
|
|
{
|
|
var query = _db.Storages.AsNoTracking().AsQueryable();
|
|
if (areaId.HasValue) query = query.Where(x => x.AreaId == areaId.Value);
|
|
if (!string.IsNullOrWhiteSpace(locationKind)) query = query.Where(x => x.LocationKind == locationKind);
|
|
return FilterByKeyword(query.OrderBy(x => x.Code), q).ToListAsync();
|
|
}
|
|
|
|
public Task<List<Container>> Containers(string? q = null) =>
|
|
FilterByKeyword(_db.Containers.AsNoTracking().OrderBy(x => x.Code), q).ToListAsync();
|
|
|
|
public Task<List<MaterialType>> MaterialTypes(string? q = null) =>
|
|
FilterByKeyword(_db.MaterialTypes.AsNoTracking().OrderBy(x => x.Code), q).ToListAsync();
|
|
|
|
public Task<List<Material>> Materials(string? q = null, string? lifecycle = null, bool? onlyUnbound = null, bool? onlyBound = null)
|
|
{
|
|
var query = _db.Materials.AsNoTracking().AsQueryable();
|
|
var life = string.IsNullOrWhiteSpace(lifecycle) ? MaterialLifecycles.Active : lifecycle.Trim();
|
|
query = query.Where(x => x.LifecycleStatus == life);
|
|
if (onlyUnbound == true || onlyBound == true)
|
|
{
|
|
var boundIds = _db.ContainerMaterials.AsNoTracking().Select(x => x.MaterialId);
|
|
if (onlyUnbound == true) query = query.Where(x => !boundIds.Contains(x.Id));
|
|
if (onlyBound == true) query = query.Where(x => boundIds.Contains(x.Id));
|
|
}
|
|
return FilterByKeyword(query.OrderBy(x => x.Code), q).ToListAsync();
|
|
}
|
|
|
|
public Task<List<ContainerLocation>> ContainerLocations(string? locationType = null, string? q = null)
|
|
{
|
|
var query = _db.ContainerLocations.AsNoTracking().OrderBy(x => x.ContainerId).AsQueryable();
|
|
if (!string.IsNullOrWhiteSpace(locationType)) query = query.Where(x => x.LocationType == locationType);
|
|
return FilterByKeyword(query, q).ToListAsync();
|
|
}
|
|
|
|
public Task<List<ContainerMaterial>> ContainerMaterials(string? q = null, Guid? containerId = null)
|
|
{
|
|
var query = _db.ContainerMaterials.AsNoTracking().AsQueryable();
|
|
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
|
|
return FilterByKeyword(query.OrderBy(x => x.ContainerId), q).ToListAsync();
|
|
}
|
|
|
|
public async Task<List<InventoryMaterialRow>> InventoryMaterials(Guid? areaId = null, Guid? storageId = null, string? q = null)
|
|
{
|
|
var binds = await _db.ContainerMaterials.AsNoTracking().ToListAsync();
|
|
var materials = await _db.Materials.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
|
var locations = await _db.ContainerLocations.AsNoTracking().ToDictionaryAsync(x => x.ContainerId);
|
|
var storages = await _db.Storages.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
|
var areas = await _db.WarehouseAreas.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
|
var containers = await _db.Containers.AsNoTracking().ToDictionaryAsync(x => x.Id);
|
|
|
|
var rows = new List<InventoryMaterialRow>();
|
|
foreach (var b in binds)
|
|
{
|
|
if (!materials.TryGetValue(b.MaterialId, out var mat)) continue;
|
|
locations.TryGetValue(b.ContainerId, out var loc);
|
|
Storage? storage = null;
|
|
WarehouseArea? area = null;
|
|
if (loc != null && loc.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(loc.LocationId, out var sid))
|
|
storages.TryGetValue(sid, out storage);
|
|
if (storage != null) areas.TryGetValue(storage.AreaId, out area);
|
|
containers.TryGetValue(b.ContainerId, out var ctn);
|
|
|
|
if (storageId.HasValue && storage?.Id != storageId.Value) continue;
|
|
if (areaId.HasValue && area?.Id != areaId.Value) continue;
|
|
if (!string.IsNullOrWhiteSpace(q))
|
|
{
|
|
var s = q.Trim();
|
|
if (!(mat.Code.Contains(s) || mat.Name.Contains(s) || (ctn?.Code.Contains(s) ?? false) || (storage?.Code.Contains(s) ?? false)))
|
|
continue;
|
|
}
|
|
|
|
rows.Add(new InventoryMaterialRow(
|
|
mat.Id, mat.Code, mat.Name, mat.Barcode, mat.TypeCode,
|
|
b.ContainerId, ctn?.Code ?? "", ctn?.Name ?? "",
|
|
storage?.Id, storage?.Code ?? "", storage?.Name ?? "",
|
|
area?.Id, area?.Code ?? "", area?.Name ?? "",
|
|
loc?.LocationType ?? "", b.BoundAt));
|
|
}
|
|
|
|
return rows.OrderBy(x => x.MaterialCode).ToList();
|
|
}
|
|
|
|
public async Task<List<StockEvent>> StockEvents(string? eventType = null, Guid? materialId = null, Guid? containerId = null)
|
|
{
|
|
var query = _db.StockEvents.AsNoTracking().AsQueryable();
|
|
if (!string.IsNullOrWhiteSpace(eventType)) query = query.Where(x => x.EventType == eventType);
|
|
if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value);
|
|
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
|
|
var rows = await query.ToListAsync();
|
|
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
|
|
}
|
|
|
|
public async Task<Warehouse> SaveWarehouse(MasterDataRequest req, string actor)
|
|
{
|
|
Warehouse entity;
|
|
if (req.Id.HasValue) entity = await FindEditable(_db.Warehouses, req.Id.Value, req.Version);
|
|
else
|
|
{
|
|
entity = new Warehouse();
|
|
StampCreate(entity, actor);
|
|
_db.Warehouses.Add(entity);
|
|
}
|
|
await EnsureUnique(_db.Warehouses, x => x.Code == req.Code && x.Id != entity.Id, "仓库编码已存在");
|
|
entity.Code = req.Code.Trim();
|
|
entity.Name = req.Name.Trim();
|
|
entity.Type = req.Type.TrimOr("Default");
|
|
entity.Enabled = req.Enabled;
|
|
entity.SortOrder = req.SortOrder;
|
|
ApplyCommon(entity, req, actor);
|
|
await _db.SaveChangesAsync();
|
|
return entity;
|
|
}
|
|
|
|
public async Task<WarehouseArea> SaveArea(AreaRequest req, string actor)
|
|
{
|
|
var warehouseId = req.WarehouseId ?? Guid.Empty;
|
|
if (warehouseId == Guid.Empty)
|
|
warehouseId = (await EnsureDefaultWarehouseAsync(actor)).Id;
|
|
else
|
|
await _refs.EnsureWarehouseAsync(warehouseId);
|
|
|
|
WarehouseArea entity;
|
|
if (req.Id.HasValue) entity = await FindEditable(_db.WarehouseAreas, req.Id.Value, req.Version);
|
|
else
|
|
{
|
|
entity = new WarehouseArea();
|
|
StampCreate(entity, actor);
|
|
_db.WarehouseAreas.Add(entity);
|
|
}
|
|
await EnsureUnique(_db.WarehouseAreas, x => x.Code == req.Code && x.Id != entity.Id, "库区编码已存在");
|
|
entity.WarehouseId = warehouseId;
|
|
entity.Code = req.Code.Trim();
|
|
entity.Name = req.Name.Trim();
|
|
entity.Type = req.Type.TrimOr("Storage");
|
|
entity.LayoutMode = AreaLayoutModes.All.Contains(req.LayoutMode) ? req.LayoutMode : AreaLayoutModes.Flat;
|
|
entity.State = req.State.TrimOr("Default");
|
|
entity.Enabled = req.Enabled;
|
|
entity.SortOrder = req.SortOrder;
|
|
ApplyCommon(entity, req, actor);
|
|
await _db.SaveChangesAsync();
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Storage> SaveStorage(StorageRequest req, string actor)
|
|
{
|
|
await _refs.EnsureAreaAsync(req.AreaId);
|
|
var kind = LocationKinds.All.Contains(req.LocationKind) ? req.LocationKind : LocationKinds.Station;
|
|
if (kind == LocationKinds.Grid)
|
|
await EnsureGridCoordUnique(req.AreaId, req.ColumnNo, req.LevelNo, req.DepthNo, req.Id);
|
|
|
|
Storage entity;
|
|
if (req.Id.HasValue) entity = await FindEditable(_db.Storages, req.Id.Value, req.Version);
|
|
else
|
|
{
|
|
entity = new Storage();
|
|
StampCreate(entity, actor);
|
|
_db.Storages.Add(entity);
|
|
}
|
|
await EnsureUnique(_db.Storages, x => x.Code == req.Code && x.Id != entity.Id, "库位编码已存在");
|
|
entity.AreaId = req.AreaId;
|
|
entity.Code = req.Code.Trim();
|
|
entity.Name = req.Name.Trim();
|
|
entity.StorageType = req.StorageType.TrimOr("Storage");
|
|
entity.LocationKind = kind;
|
|
entity.ColumnNo = req.ColumnNo;
|
|
entity.LevelNo = req.LevelNo <= 0 ? 1 : req.LevelNo;
|
|
entity.DepthNo = req.DepthNo <= 0 ? 1 : req.DepthNo;
|
|
entity.SiteId = req.SiteId.TrimOr("");
|
|
entity.SiteCode = req.SiteCode.TrimOr(entity.SiteId);
|
|
entity.Barcode = req.Barcode.TrimOr("");
|
|
entity.Capacity = 1;
|
|
var status = StorageStatuses.Normalize(req.Status.TrimOr(StorageStatuses.Empty));
|
|
if (status == StorageStatuses.Disabled || req.Enabled == false)
|
|
entity.Status = req.Enabled ? status : StorageStatuses.Disabled;
|
|
else if (!string.IsNullOrWhiteSpace(req.Status) && StorageStatuses.All.Contains(status))
|
|
entity.Status = status;
|
|
entity.Usage = req.Usage.TrimOr("");
|
|
entity.Priority = req.Priority;
|
|
entity.ZoneCode = req.ZoneCode.TrimOr("");
|
|
entity.AllowInbound = req.AllowInbound;
|
|
entity.AllowOutbound = req.AllowOutbound;
|
|
entity.Enabled = req.Enabled;
|
|
ApplyCommon(entity, req, actor);
|
|
await _db.SaveChangesAsync();
|
|
if (entity.Status != StorageStatuses.Disabled)
|
|
await SyncOccupancyStatus(storageId: entity.Id);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<int> GenerateBins(Guid areaId, GenerateBinsRequest req, string actor)
|
|
{
|
|
await _refs.EnsureAreaAsync(areaId);
|
|
if (req.ColumnFrom > req.ColumnTo || req.LevelFrom > req.LevelTo || req.DepthFrom > req.DepthTo)
|
|
throw new InvalidOperationException("生成范围无效");
|
|
var area = await _db.WarehouseAreas.AsNoTracking().FirstAsync(x => x.Id == areaId);
|
|
var existing = await _db.Storages.AsNoTracking()
|
|
.Where(x => x.AreaId == areaId && x.LocationKind == LocationKinds.Grid)
|
|
.Select(x => new { x.ColumnNo, x.LevelNo, x.DepthNo })
|
|
.ToListAsync();
|
|
var set = existing.Select(x => (x.ColumnNo, x.LevelNo, x.DepthNo)).ToHashSet();
|
|
var created = 0;
|
|
for (var col = req.ColumnFrom; col <= req.ColumnTo; col++)
|
|
for (var level = req.LevelFrom; level <= req.LevelTo; level++)
|
|
for (var depth = req.DepthFrom; depth <= req.DepthTo; depth++)
|
|
{
|
|
if (set.Contains((col, level, depth))) continue;
|
|
var code = string.IsNullOrWhiteSpace(req.CodePattern)
|
|
? $"{area.Code}-{level:D2}{col:D2}" + (req.DepthFrom == req.DepthTo && depth == 1 ? "" : $"-{depth}")
|
|
: req.CodePattern
|
|
.Replace("{Area}", area.Code, StringComparison.OrdinalIgnoreCase)
|
|
.Replace("{Level}", level.ToString("D2"), StringComparison.OrdinalIgnoreCase)
|
|
.Replace("{Column}", col.ToString("D2"), StringComparison.OrdinalIgnoreCase)
|
|
.Replace("{Depth}", depth.ToString(), StringComparison.OrdinalIgnoreCase);
|
|
if (await _db.Storages.AnyAsync(x => x.Code == code))
|
|
code = $"{code}-{Guid.NewGuid().ToString("N")[..4]}";
|
|
var entity = new Storage
|
|
{
|
|
AreaId = areaId,
|
|
Code = code,
|
|
Name = code,
|
|
LocationKind = LocationKinds.Grid,
|
|
ColumnNo = col,
|
|
LevelNo = level,
|
|
DepthNo = depth,
|
|
Status = StorageStatuses.Empty,
|
|
Capacity = 1,
|
|
Enabled = true,
|
|
AllowInbound = true,
|
|
AllowOutbound = true
|
|
};
|
|
StampCreate(entity, actor);
|
|
_db.Storages.Add(entity);
|
|
created++;
|
|
}
|
|
await _db.SaveChangesAsync();
|
|
return created;
|
|
}
|
|
|
|
public async Task<Storage> SetStorageLock(Guid id, bool isLock, long? version, string actor)
|
|
{
|
|
var entity = await FindEditable(_db.Storages, id, version);
|
|
entity.IsLock = isLock;
|
|
entity.UpdatedBy = actor;
|
|
await _db.SaveChangesAsync();
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Storage> SetStorageEnabled(Guid id, bool enabled, long? version, string actor)
|
|
{
|
|
var entity = await FindEditable(_db.Storages, id, version);
|
|
entity.Enabled = enabled;
|
|
entity.Status = enabled
|
|
? (entity.Status == StorageStatuses.Disabled ? StorageStatuses.Empty : entity.Status)
|
|
: StorageStatuses.Disabled;
|
|
entity.UpdatedBy = actor;
|
|
await _db.SaveChangesAsync();
|
|
if (enabled) await SyncOccupancyStatus(storageId: id);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Container> SaveContainer(ContainerRequest req, string actor)
|
|
{
|
|
Container entity;
|
|
if (req.Id.HasValue) entity = await FindEditable(_db.Containers, req.Id.Value, req.Version);
|
|
else
|
|
{
|
|
entity = new Container();
|
|
StampCreate(entity, actor);
|
|
_db.Containers.Add(entity);
|
|
}
|
|
await EnsureUnique(_db.Containers, x => x.Code == req.Code && x.Id != entity.Id, "容器编码已存在");
|
|
entity.AreaId = req.AreaId;
|
|
entity.Code = req.Code.Trim();
|
|
entity.Name = req.Name.Trim();
|
|
entity.ContainerType = req.Type.TrimOr("Box");
|
|
var status = req.Status.TrimOr(ContainerStatuses.EmptyMaterial);
|
|
entity.Status = ContainerStatuses.All.Contains(status) ? status : ContainerStatuses.EmptyMaterial;
|
|
entity.Barcode = req.Barcode.TrimOr("");
|
|
entity.Length = req.Length;
|
|
entity.Width = req.Width;
|
|
entity.Height = req.Height;
|
|
entity.Enabled = req.Enabled;
|
|
ApplyCommon(entity, req, actor);
|
|
await _db.SaveChangesAsync();
|
|
await SyncOccupancyStatus(containerId: entity.Id);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<MaterialType> SaveMaterialType(MaterialTypeRequest req, string actor)
|
|
{
|
|
MaterialType entity;
|
|
if (req.Id.HasValue) entity = await FindEditable(_db.MaterialTypes, req.Id.Value, req.Version);
|
|
else
|
|
{
|
|
entity = new MaterialType();
|
|
StampCreate(entity, actor);
|
|
_db.MaterialTypes.Add(entity);
|
|
}
|
|
await EnsureUnique(_db.MaterialTypes, x => x.Code == req.Code && x.Id != entity.Id, "物料类型编码已存在");
|
|
entity.Code = req.Code.Trim();
|
|
entity.Name = req.Name.Trim();
|
|
entity.Spec = req.Spec.TrimOr("");
|
|
entity.Unit = req.Unit.TrimOr("pcs");
|
|
entity.Category = req.Category.TrimOr("");
|
|
entity.BarcodePrefix = req.BarcodePrefix.TrimOr("");
|
|
entity.Enabled = req.Enabled;
|
|
ApplyCommon(entity, req, actor);
|
|
await _db.SaveChangesAsync();
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Material> SaveMaterial(MaterialRequest req, string actor)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(req.TypeCode))
|
|
await _refs.EnsureMaterialTypeCodeAsync(req.TypeCode);
|
|
Material entity;
|
|
if (req.Id.HasValue) entity = await FindEditable(_db.Materials, req.Id.Value, req.Version);
|
|
else
|
|
{
|
|
entity = new Material();
|
|
StampCreate(entity, actor);
|
|
_db.Materials.Add(entity);
|
|
}
|
|
await EnsureUnique(_db.Materials, x => x.Code == req.Code && x.Id != entity.Id, "物料编码已存在");
|
|
if (!string.IsNullOrWhiteSpace(req.Barcode))
|
|
await EnsureUnique(_db.Materials, x => x.Barcode == req.Barcode && x.Id != entity.Id, "物料条码已存在");
|
|
entity.Code = req.Code.Trim();
|
|
entity.Name = req.Name.Trim();
|
|
entity.TypeCode = req.TypeCode.TrimOr("");
|
|
entity.Barcode = req.Barcode.TrimOr("");
|
|
entity.Spec = req.Spec.TrimOr("");
|
|
entity.Unit = req.Unit.TrimOr("pcs");
|
|
entity.Category = req.Category.TrimOr("");
|
|
var life = req.LifecycleStatus.TrimOr(MaterialLifecycles.Active);
|
|
entity.LifecycleStatus = MaterialLifecycles.All.Contains(life) ? life : MaterialLifecycles.Active;
|
|
entity.Enabled = req.Enabled;
|
|
ApplyCommon(entity, req, actor);
|
|
await _db.SaveChangesAsync();
|
|
return entity;
|
|
}
|
|
|
|
public async Task<Material> ArchiveMaterial(Guid id, long? version, string actor)
|
|
{
|
|
var entity = await FindEditable(_db.Materials, id, version);
|
|
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id))
|
|
throw new InvalidOperationException("物料仍在绑定中,不能归档");
|
|
entity.LifecycleStatus = MaterialLifecycles.Archived;
|
|
entity.UnboundAt ??= DateTimeOffset.UtcNow;
|
|
entity.UpdatedBy = actor;
|
|
await _db.SaveChangesAsync();
|
|
return entity;
|
|
}
|
|
|
|
public async Task PurgeArchivedMaterials(string actor, int take = 200)
|
|
{
|
|
var rows = await _db.Materials
|
|
.Where(x => x.LifecycleStatus == MaterialLifecycles.Archived)
|
|
.OrderBy(x => x.UpdatedAt)
|
|
.Take(take)
|
|
.ToListAsync();
|
|
foreach (var entity in rows)
|
|
{
|
|
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == entity.Id)) continue;
|
|
entity.IsDeleted = true;
|
|
entity.DeletedAt = DateTimeOffset.UtcNow;
|
|
entity.DeletedBy = actor;
|
|
entity.UpdatedBy = actor;
|
|
}
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task DeleteEntity<T>(Guid id, long? version, string actor) where T : EntityBase
|
|
{
|
|
var set = _db.Set<T>();
|
|
var entity = await FindEditable(set, id, version);
|
|
if (typeof(T) == typeof(WarehouseArea))
|
|
{
|
|
if (await _db.Storages.AnyAsync(x => x.AreaId == id))
|
|
throw new InvalidOperationException("库区下仍有货位,不能删除");
|
|
}
|
|
if (typeof(T) == typeof(Storage))
|
|
{
|
|
var sid = id.ToString("D");
|
|
if (await _db.ContainerLocations.AnyAsync(x => x.LocationType == ContainerLocationTypes.Storage && x.LocationId == sid))
|
|
throw new InvalidOperationException("货位仍被容器占用,不能删除");
|
|
}
|
|
if (typeof(T) == typeof(Material))
|
|
{
|
|
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id))
|
|
throw new InvalidOperationException("物料仍在绑定中,不能删除");
|
|
}
|
|
entity.IsDeleted = true;
|
|
entity.DeletedAt = DateTimeOffset.UtcNow;
|
|
entity.DeletedBy = actor;
|
|
entity.UpdatedBy = actor;
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task<ContainerLocation> BindOrTransferLocation(ContainerLocationRequest req, string actor)
|
|
{
|
|
await _refs.EnsureContainerAsync(req.ContainerId);
|
|
var (code, name) = await _refs.ResolveLocationSnapshotAsync(req.LocationType, req.LocationId);
|
|
if (!ContainerLocationStatuses.All.Contains(req.Status))
|
|
throw new InvalidOperationException("容器位置状态无效");
|
|
|
|
Guid? fromStorageId = null;
|
|
Guid? toStorageId = null;
|
|
if (string.Equals(req.LocationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase) &&
|
|
Guid.TryParse(req.LocationId, out var targetStorageId))
|
|
{
|
|
toStorageId = targetStorageId;
|
|
var storage = await _db.Storages.FirstOrDefaultAsync(x => x.Id == targetStorageId)
|
|
?? throw new InvalidOperationException("库位不存在");
|
|
if (!storage.Enabled || storage.Status == StorageStatuses.Disabled || storage.IsLock)
|
|
throw new InvalidOperationException("目标库位已停用或锁定");
|
|
if (!storage.AllowInbound)
|
|
throw new InvalidOperationException("目标库位不允许入库");
|
|
var occupied = await _db.ContainerLocations.AsNoTracking()
|
|
.AnyAsync(x => x.LocationType == ContainerLocationTypes.Storage &&
|
|
x.LocationId == req.LocationId &&
|
|
x.ContainerId != req.ContainerId);
|
|
if (occupied)
|
|
throw new InvalidOperationException("目标库位已被其他容器占用,库位与容器为 1 对 1");
|
|
}
|
|
|
|
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == req.ContainerId);
|
|
var before = current == null ? null : Snapshot(current);
|
|
if (before != null && before.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(before.LocationId, out var fs))
|
|
fromStorageId = fs;
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
if (current == null)
|
|
{
|
|
current = new ContainerLocation { ContainerId = req.ContainerId, EnteredAt = now };
|
|
StampCreate(current, actor);
|
|
_db.ContainerLocations.Add(current);
|
|
}
|
|
else
|
|
{
|
|
EnsureVersion(current, req.Version);
|
|
EnsureUnlocked(current);
|
|
}
|
|
|
|
current.LocationType = req.LocationType;
|
|
current.LocationId = req.LocationId.Trim();
|
|
current.LocationCode = code;
|
|
current.LocationName = name;
|
|
current.Status = req.Status;
|
|
current.EnteredAt = req.EnteredAt ?? now;
|
|
ApplyCommon(current, req, actor);
|
|
|
|
_db.ContainerLocationHistories.Add(new ContainerLocationHistory
|
|
{
|
|
RelationId = current.Id,
|
|
ContainerId = current.ContainerId,
|
|
EventType = before == null ? "Bind" : "Transfer",
|
|
FromLocationType = before?.LocationType ?? "",
|
|
FromLocationId = before?.LocationId ?? "",
|
|
ToLocationType = current.LocationType,
|
|
ToLocationId = current.LocationId,
|
|
BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json),
|
|
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
|
|
Operator = actor,
|
|
OperatedAt = now,
|
|
Source = req.Source.TrimOr("Manual"),
|
|
Reason = req.Reason.TrimOr(""),
|
|
Remark = req.Remark.TrimOr(""),
|
|
Extend = NormalizeExtend(req.Extend)
|
|
});
|
|
|
|
await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now);
|
|
try
|
|
{
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
|
{
|
|
throw new InvalidOperationException("目标库位已被其他容器占用,库位与容器为 1 对 1");
|
|
}
|
|
await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId);
|
|
await SyncOccupancyStatus(storageId: toStorageId);
|
|
return current;
|
|
}
|
|
|
|
public async Task UnbindLocation(Guid containerId, string actor, string reason = "")
|
|
{
|
|
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == containerId)
|
|
?? throw new InvalidOperationException("容器当前位置不存在");
|
|
EnsureUnlocked(current);
|
|
Guid? fromStorageId = null;
|
|
if (current.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(current.LocationId, out var fs))
|
|
fromStorageId = fs;
|
|
var before = Snapshot(current);
|
|
var now = DateTimeOffset.UtcNow;
|
|
_db.ContainerLocationHistories.Add(new ContainerLocationHistory
|
|
{
|
|
RelationId = current.Id,
|
|
ContainerId = current.ContainerId,
|
|
EventType = "Unbind",
|
|
FromLocationType = current.LocationType,
|
|
FromLocationId = current.LocationId,
|
|
BeforeJson = JsonSerializer.Serialize(before, _json),
|
|
AfterJson = "{}",
|
|
Operator = actor,
|
|
OperatedAt = now,
|
|
Source = "Manual",
|
|
Reason = reason
|
|
});
|
|
await AddContainerMoveEventAsync(containerId, fromStorageId, null, actor, reason, now);
|
|
_db.ContainerLocations.Remove(current);
|
|
await _db.SaveChangesAsync();
|
|
await SyncOccupancyStatus(containerId: containerId, storageId: fromStorageId);
|
|
}
|
|
|
|
public async Task<ContainerMaterial> BindMaterial(BindMaterialRequest req, string actor)
|
|
{
|
|
await _refs.EnsureContainerAsync(req.ContainerId);
|
|
var material = await _db.Materials.FirstOrDefaultAsync(x => x.Id == req.MaterialId)
|
|
?? throw new InvalidOperationException("物料不存在");
|
|
if (material.LifecycleStatus != MaterialLifecycles.Active)
|
|
throw new InvalidOperationException("仅 Active 物料可绑定");
|
|
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == req.MaterialId))
|
|
throw new InvalidOperationException("物料已绑定其他容器");
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
var current = new ContainerMaterial
|
|
{
|
|
ContainerId = req.ContainerId,
|
|
MaterialId = req.MaterialId,
|
|
Quantity = 1,
|
|
Status = ContainerMaterialStatuses.Bound,
|
|
BoundAt = now,
|
|
LoadedAt = now
|
|
};
|
|
StampCreate(current, actor);
|
|
ApplyCommon(current, req, actor);
|
|
_db.ContainerMaterials.Add(current);
|
|
|
|
_db.ContainerMaterialHistories.Add(new ContainerMaterialHistory
|
|
{
|
|
RelationId = current.Id,
|
|
ContainerId = current.ContainerId,
|
|
MaterialId = current.MaterialId,
|
|
EventType = "Bind",
|
|
QuantityDelta = 0,
|
|
BeforeJson = "{}",
|
|
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
|
|
Operator = actor,
|
|
OperatedAt = now,
|
|
Source = req.Source.TrimOr("Manual"),
|
|
Reason = req.Reason.TrimOr(""),
|
|
Remark = req.Remark.TrimOr(""),
|
|
Extend = NormalizeExtend(req.Extend)
|
|
});
|
|
|
|
material.UnboundAt = null;
|
|
await AddBindUnbindEventAsync(StockEventTypes.Bind, material, req.ContainerId, actor, req.Reason.TrimOr(""), now);
|
|
await _db.SaveChangesAsync();
|
|
await SyncOccupancyStatus(containerId: req.ContainerId);
|
|
return current;
|
|
}
|
|
|
|
/// <summary>兼容旧接口:忽略数量,按实体绑定。</summary>
|
|
public Task<ContainerMaterial> SaveContainerMaterial(ContainerMaterialRequest req, string actor) =>
|
|
BindMaterial(new BindMaterialRequest(req.Id, req.Version, req.ContainerId, req.MaterialId, req.Source, req.Reason, req.IsLock, req.Remark, req.Extend), actor);
|
|
|
|
public async Task UnbindMaterial(Guid bindingId, string actor, string reason = "", bool archive = false)
|
|
{
|
|
var current = await _db.ContainerMaterials.FirstOrDefaultAsync(x => x.Id == bindingId)
|
|
?? throw new InvalidOperationException("绑定不存在");
|
|
EnsureUnlocked(current);
|
|
var material = await _db.Materials.FirstOrDefaultAsync(x => x.Id == current.MaterialId);
|
|
var before = Snapshot(current);
|
|
var now = DateTimeOffset.UtcNow;
|
|
var containerId = current.ContainerId;
|
|
|
|
_db.ContainerMaterialHistories.Add(new ContainerMaterialHistory
|
|
{
|
|
RelationId = current.Id,
|
|
ContainerId = current.ContainerId,
|
|
MaterialId = current.MaterialId,
|
|
EventType = "Unbind",
|
|
QuantityDelta = 0,
|
|
BeforeJson = JsonSerializer.Serialize(before, _json),
|
|
AfterJson = "{}",
|
|
Operator = actor,
|
|
OperatedAt = now,
|
|
Source = "Manual",
|
|
Reason = reason
|
|
});
|
|
|
|
if (material != null)
|
|
{
|
|
material.UnboundAt = now;
|
|
if (archive) material.LifecycleStatus = MaterialLifecycles.Archived;
|
|
await AddBindUnbindEventAsync(StockEventTypes.Unbind, material, containerId, actor, reason, now);
|
|
}
|
|
|
|
_db.ContainerMaterials.Remove(current);
|
|
await _db.SaveChangesAsync();
|
|
await SyncOccupancyStatus(containerId: containerId);
|
|
}
|
|
|
|
public Task UnloadMaterial(Guid id, string actor, string reason = "") => UnbindMaterial(id, actor, reason);
|
|
|
|
public async Task SyncOccupancyStatus(Guid? storageId = null, Guid? containerId = null)
|
|
{
|
|
var containerIds = new HashSet<Guid>();
|
|
if (containerId.HasValue) containerIds.Add(containerId.Value);
|
|
|
|
if (storageId.HasValue)
|
|
{
|
|
var sid = storageId.Value.ToString("D");
|
|
var loc = await _db.ContainerLocations.FirstOrDefaultAsync(x =>
|
|
x.LocationType == ContainerLocationTypes.Storage && x.LocationId == sid);
|
|
if (loc != null) containerIds.Add(loc.ContainerId);
|
|
|
|
var storage = await _db.Storages.FirstOrDefaultAsync(x => x.Id == storageId.Value);
|
|
if (storage != null && storage.Status != StorageStatuses.Disabled)
|
|
{
|
|
if (loc == null) storage.Status = StorageStatuses.Empty;
|
|
else
|
|
{
|
|
var hasMat = await _db.ContainerMaterials.AnyAsync(x => x.ContainerId == loc.ContainerId);
|
|
storage.Status = hasMat ? StorageStatuses.FullContainer : StorageStatuses.EmptyContainer;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var cid in containerIds)
|
|
{
|
|
var ctn = await _db.Containers.FirstOrDefaultAsync(x => x.Id == cid);
|
|
if (ctn == null) continue;
|
|
var hasMat = await _db.ContainerMaterials.AnyAsync(x => x.ContainerId == cid);
|
|
ctn.Status = hasMat ? ContainerStatuses.FullMaterial : ContainerStatuses.EmptyMaterial;
|
|
|
|
var loc = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == cid);
|
|
if (loc != null && loc.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(loc.LocationId, out var sid2))
|
|
{
|
|
var storage = await _db.Storages.FirstOrDefaultAsync(x => x.Id == sid2);
|
|
if (storage != null && storage.Status != StorageStatuses.Disabled)
|
|
storage.Status = hasMat ? StorageStatuses.FullContainer : StorageStatuses.EmptyContainer;
|
|
}
|
|
}
|
|
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task<Warehouse> EnsureDefaultWarehouseAsync(string actor = "system")
|
|
{
|
|
var existing = await _db.Warehouses.FirstOrDefaultAsync(x => x.Code == WmsDefaults.DefaultWarehouseCode);
|
|
if (existing != null) return existing;
|
|
var wh = new Warehouse
|
|
{
|
|
Code = WmsDefaults.DefaultWarehouseCode,
|
|
Name = WmsDefaults.DefaultWarehouseName,
|
|
Type = "Default",
|
|
Enabled = true,
|
|
SortOrder = 0
|
|
};
|
|
StampCreate(wh, actor);
|
|
_db.Warehouses.Add(wh);
|
|
await _db.SaveChangesAsync();
|
|
return wh;
|
|
}
|
|
|
|
public async Task MigrateLegacyAsync(string actor = "system")
|
|
{
|
|
var wh = await EnsureDefaultWarehouseAsync(actor);
|
|
var areas = await _db.WarehouseAreas.Where(x => x.WarehouseId == Guid.Empty).ToListAsync();
|
|
foreach (var a in areas)
|
|
{
|
|
a.WarehouseId = wh.Id;
|
|
if (string.IsNullOrWhiteSpace(a.LayoutMode)) a.LayoutMode = AreaLayoutModes.Flat;
|
|
}
|
|
|
|
var storages = await _db.Storages.ToListAsync();
|
|
foreach (var s in storages)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(s.LocationKind))
|
|
s.LocationKind = LocationKinds.Station;
|
|
if (s.LevelNo <= 0) s.LevelNo = 1;
|
|
if (s.DepthNo <= 0) s.DepthNo = 1;
|
|
if (string.IsNullOrWhiteSpace(s.SiteCode)) s.SiteCode = s.SiteId;
|
|
s.Status = StorageStatuses.Normalize(s.Status) switch
|
|
{
|
|
StorageStatuses.Available or StorageStatuses.Idle => StorageStatuses.Empty,
|
|
StorageStatuses.Occupied => StorageStatuses.FullContainer,
|
|
var x => x
|
|
};
|
|
if (!s.Enabled) s.Status = StorageStatuses.Disabled;
|
|
}
|
|
|
|
var containers = await _db.Containers.ToListAsync();
|
|
foreach (var c in containers)
|
|
{
|
|
c.Status = c.Status switch
|
|
{
|
|
"Idle" or "Empty" => ContainerStatuses.EmptyMaterial,
|
|
"Loaded" => ContainerStatuses.FullMaterial,
|
|
_ when ContainerStatuses.All.Contains(c.Status) => c.Status,
|
|
_ => ContainerStatuses.EmptyMaterial
|
|
};
|
|
}
|
|
|
|
var materials = await _db.Materials.Where(x => string.IsNullOrWhiteSpace(x.LifecycleStatus)).ToListAsync();
|
|
foreach (var m in materials)
|
|
m.LifecycleStatus = MaterialLifecycles.Active;
|
|
|
|
var binds = await _db.ContainerMaterials.ToListAsync();
|
|
foreach (var b in binds)
|
|
{
|
|
b.Quantity = 1;
|
|
if (b.BoundAt == default) b.BoundAt = b.LoadedAt == default ? DateTimeOffset.UtcNow : b.LoadedAt;
|
|
if (!ContainerMaterialStatuses.ActiveBind.Contains(b.Status))
|
|
b.Status = ContainerMaterialStatuses.Bound;
|
|
}
|
|
|
|
await _db.SaveChangesAsync();
|
|
|
|
foreach (var s in storages.Where(x => x.Status != StorageStatuses.Disabled))
|
|
await SyncOccupancyStatus(storageId: s.Id);
|
|
}
|
|
|
|
public async Task<List<ContainerLocationHistory>> LocationHistory(Guid? containerId = null)
|
|
{
|
|
var rows = await (containerId.HasValue
|
|
? _db.ContainerLocationHistories.AsNoTracking().Where(x => x.ContainerId == containerId.Value)
|
|
: _db.ContainerLocationHistories.AsNoTracking())
|
|
.ToListAsync();
|
|
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
|
|
}
|
|
|
|
public async Task<List<ContainerMaterialHistory>> MaterialHistory(Guid? containerId = null, Guid? materialId = null)
|
|
{
|
|
var query = _db.ContainerMaterialHistories.AsNoTracking().AsQueryable();
|
|
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
|
|
if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value);
|
|
var rows = await query.ToListAsync();
|
|
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
|
|
}
|
|
|
|
private async Task EnsureGridCoordUnique(Guid areaId, int column, int level, int depth, Guid? excludeId)
|
|
{
|
|
var exists = await _db.Storages.AnyAsync(x =>
|
|
x.AreaId == areaId &&
|
|
x.LocationKind == LocationKinds.Grid &&
|
|
x.ColumnNo == column && x.LevelNo == level && x.DepthNo == depth &&
|
|
(!excludeId.HasValue || x.Id != excludeId.Value));
|
|
if (exists) throw new InvalidOperationException("同库区网格坐标已存在");
|
|
}
|
|
|
|
private async Task AddBindUnbindEventAsync(string eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now)
|
|
{
|
|
var ctn = await _db.Containers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == containerId);
|
|
var loc = await _db.ContainerLocations.AsNoTracking().FirstOrDefaultAsync(x => x.ContainerId == containerId);
|
|
Storage? storage = null;
|
|
WarehouseArea? area = null;
|
|
if (loc is { LocationType: ContainerLocationTypes.Storage } && Guid.TryParse(loc.LocationId, out var sid))
|
|
{
|
|
storage = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == sid);
|
|
if (storage != null)
|
|
area = await _db.WarehouseAreas.AsNoTracking().FirstOrDefaultAsync(x => x.Id == storage.AreaId);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(material.Code))
|
|
throw new InvalidOperationException("物料编码快照不能为空");
|
|
|
|
_db.StockEvents.Add(new StockEvent
|
|
{
|
|
EventType = eventType,
|
|
MaterialId = material.Id,
|
|
MaterialCode = material.Code,
|
|
MaterialName = material.Name,
|
|
MaterialBarcode = material.Barcode,
|
|
MaterialTypeCode = material.TypeCode,
|
|
ContainerId = containerId,
|
|
ContainerCode = ctn?.Code ?? "",
|
|
ContainerName = ctn?.Name ?? "",
|
|
StorageId = storage?.Id,
|
|
StorageCode = storage?.Code ?? loc?.LocationCode ?? "",
|
|
StorageName = storage?.Name ?? loc?.LocationName ?? "",
|
|
AreaCode = area?.Code ?? "",
|
|
RefType = "Manual",
|
|
Operator = actor,
|
|
OperatedAt = now,
|
|
Reason = reason
|
|
});
|
|
}
|
|
|
|
private async Task AddContainerMoveEventAsync(Guid containerId, Guid? fromStorageId, Guid? toStorageId, string actor, string reason, DateTimeOffset now)
|
|
{
|
|
var ctn = await _db.Containers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == containerId)
|
|
?? throw new InvalidOperationException("容器不存在");
|
|
Storage? from = null, to = null;
|
|
if (fromStorageId.HasValue)
|
|
from = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == fromStorageId.Value);
|
|
if (toStorageId.HasValue)
|
|
to = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == toStorageId.Value);
|
|
|
|
_db.StockEvents.Add(new StockEvent
|
|
{
|
|
EventType = StockEventTypes.ContainerMove,
|
|
ContainerId = containerId,
|
|
ContainerCode = ctn.Code,
|
|
ContainerName = ctn.Name,
|
|
FromStorageId = from?.Id,
|
|
FromStorageCode = from?.Code ?? "",
|
|
FromStorageName = from?.Name ?? "",
|
|
ToStorageId = to?.Id,
|
|
ToStorageCode = to?.Code ?? "",
|
|
ToStorageName = to?.Name ?? "",
|
|
StorageId = to?.Id ?? from?.Id,
|
|
StorageCode = to?.Code ?? from?.Code ?? "",
|
|
StorageName = to?.Name ?? from?.Name ?? "",
|
|
RefType = "Manual",
|
|
Operator = actor,
|
|
OperatedAt = now,
|
|
Reason = reason
|
|
});
|
|
}
|
|
|
|
private async Task<T> FindEditable<T>(DbSet<T> set, Guid id, long? version) where T : EntityBase
|
|
{
|
|
var entity = await set.FirstOrDefaultAsync(x => x.Id == id) ?? throw new InvalidOperationException("数据不存在");
|
|
EnsureVersion(entity, version);
|
|
EnsureUnlocked(entity);
|
|
return entity;
|
|
}
|
|
|
|
private static void EnsureVersion(EntityBase entity, long? version)
|
|
{
|
|
if (version.HasValue && entity.Version != version.Value)
|
|
throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试");
|
|
}
|
|
|
|
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
|
|
{
|
|
for (Exception? e = ex; e != null; e = e.InnerException)
|
|
{
|
|
var msg = e.Message;
|
|
if (msg.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase)
|
|
|| msg.Contains("unique index", StringComparison.OrdinalIgnoreCase)
|
|
|| msg.Contains("duplicate key", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void EnsureUnlocked(EntityBase entity)
|
|
{
|
|
if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改");
|
|
}
|
|
|
|
private static void StampCreate(EntityBase entity, string actor)
|
|
{
|
|
entity.CreatedBy = actor;
|
|
entity.UpdatedBy = actor;
|
|
}
|
|
|
|
private void ApplyCommon(EntityBase entity, CommonRequest req, string actor)
|
|
{
|
|
entity.IsLock = req.IsLock;
|
|
entity.Remark = req.Remark.TrimOr("");
|
|
entity.Extend = NormalizeExtend(req.Extend);
|
|
entity.UpdatedBy = actor;
|
|
}
|
|
|
|
private static async Task EnsureUnique<T>(IQueryable<T> query, System.Linq.Expressions.Expression<Func<T, bool>> predicate, string message)
|
|
{
|
|
if (await query.AnyAsync(predicate)) throw new InvalidOperationException(message);
|
|
}
|
|
|
|
private string NormalizeExtend(string? extend)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(extend)) return "{}";
|
|
if (extend.Length > 4000) throw new InvalidOperationException("扩展字段过长");
|
|
using var doc = JsonDocument.Parse(extend);
|
|
if (doc.RootElement.ValueKind != JsonValueKind.Object) throw new InvalidOperationException("扩展字段必须是 JSON object");
|
|
return extend;
|
|
}
|
|
|
|
private static IQueryable<T> FilterByKeyword<T>(IQueryable<T> query, string? q)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(q)) return query;
|
|
var s = q.Trim();
|
|
return typeof(T).Name switch
|
|
{
|
|
nameof(Warehouse) => (IQueryable<T>)((IQueryable<Warehouse>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s)),
|
|
nameof(WarehouseArea) => (IQueryable<T>)((IQueryable<WarehouseArea>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s)),
|
|
nameof(Storage) => (IQueryable<T>)((IQueryable<Storage>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s) || x.SiteId.Contains(s) || x.Barcode.Contains(s)),
|
|
nameof(Container) => (IQueryable<T>)((IQueryable<Container>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s) || x.Barcode.Contains(s)),
|
|
nameof(MaterialType) => (IQueryable<T>)((IQueryable<MaterialType>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s)),
|
|
nameof(Material) => (IQueryable<T>)((IQueryable<Material>)query).Where(x => x.Code.Contains(s) || x.Name.Contains(s) || x.Spec.Contains(s) || x.Barcode.Contains(s)),
|
|
nameof(ContainerLocation) => (IQueryable<T>)((IQueryable<ContainerLocation>)query).Where(x => x.LocationCode.Contains(s) || x.LocationName.Contains(s)),
|
|
nameof(ContainerMaterial) => query,
|
|
_ => query
|
|
};
|
|
}
|
|
|
|
private static ContainerLocationSnapshot Snapshot(ContainerLocation x) => new(
|
|
x.Id, x.ContainerId, x.LocationType, x.LocationId, x.LocationCode, x.LocationName, x.Status, x.EnteredAt, x.Version);
|
|
|
|
private static ContainerMaterialSnapshot Snapshot(ContainerMaterial x) => new(
|
|
x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status, x.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version);
|
|
}
|
|
|
|
public sealed record InventoryMaterialRow(
|
|
Guid MaterialId, string MaterialCode, string MaterialName, string MaterialBarcode, string TypeCode,
|
|
Guid ContainerId, string ContainerCode, string ContainerName,
|
|
Guid? StorageId, string StorageCode, string StorageName,
|
|
Guid? AreaId, string AreaCode, string AreaName,
|
|
string LocationType, DateTimeOffset BoundAt);
|
|
|
|
public sealed record ContainerLocationSnapshot(
|
|
Guid Id, Guid ContainerId, string LocationType, string LocationId, string LocationCode, string LocationName,
|
|
string Status, DateTimeOffset EnteredAt, long Version);
|
|
|
|
public sealed record ContainerMaterialSnapshot(
|
|
Guid Id, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo, string Status,
|
|
DateTimeOffset BoundAt, DateTimeOffset LoadedAt, DateTimeOffset? UnloadedAt, long Version);
|
|
|
|
public abstract record CommonRequest(Guid? Id, long? Version, bool IsLock, string Remark, string Extend);
|
|
|
|
public sealed record MasterDataRequest(
|
|
Guid? Id, long? Version, string Code, string Name, string Type, string Status, bool Enabled, int SortOrder,
|
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record AreaRequest(
|
|
Guid? Id, long? Version, Guid? WarehouseId, string Code, string Name, string Type, string LayoutMode, string State,
|
|
bool Enabled, int SortOrder, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record StorageRequest(
|
|
Guid? Id, long? Version, Guid AreaId, string Code, string Name, string StorageType, string LocationKind,
|
|
int ColumnNo, int LevelNo, int DepthNo, string SiteId, string SiteCode, string Barcode, int Capacity,
|
|
string Status, string Usage, int Priority, string ZoneCode, bool AllowInbound, bool AllowOutbound, bool Enabled,
|
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record GenerateBinsRequest(int ColumnFrom, int ColumnTo, int LevelFrom, int LevelTo, int DepthFrom, int DepthTo, string? CodePattern);
|
|
|
|
public sealed record ContainerRequest(
|
|
Guid? Id, long? Version, Guid? AreaId, string Code, string Name, string Type, string Status, string Barcode,
|
|
double Length, double Width, double Height, bool Enabled, bool IsLock, string Remark, string Extend)
|
|
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record MaterialTypeRequest(
|
|
Guid? Id, long? Version, string Code, string Name, string Spec, string Unit, string Category, string BarcodePrefix,
|
|
bool Enabled, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record MaterialRequest(
|
|
Guid? Id, long? Version, string Code, string Name, string TypeCode, string Barcode, string Spec, string Unit,
|
|
string Category, string LifecycleStatus, bool Enabled, bool IsLock, string Remark, string Extend)
|
|
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record ContainerLocationRequest(
|
|
Guid? Id, long? Version, Guid ContainerId, string LocationType, string LocationId, string Status,
|
|
DateTimeOffset? EnteredAt, string Source, string Reason, bool IsLock, string Remark, string Extend)
|
|
: CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record BindMaterialRequest(
|
|
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, string Source, string Reason,
|
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public sealed record ContainerMaterialRequest(
|
|
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo,
|
|
string Status, DateTimeOffset? LoadedAt, DateTimeOffset? UnloadedAt, string Source, string Reason,
|
|
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
|
|
|
|
public static class WmsStringExtensions
|
|
{
|
|
public static string TrimOr(this string? value, string fallback) =>
|
|
string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
|
|
|
public static string ActorName(this ClaimsPrincipal user) =>
|
|
user.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.UniqueName)?.Value
|
|
?? user.Identity?.Name
|
|
?? "system";
|
|
}
|