Complete all for Items
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
@@ -23,6 +24,7 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
private readonly IRepository<Warehouse> _warehouses;
|
||||
private readonly IRepository<Item> _items;
|
||||
private readonly IRepository<ReasonCode> _reasonCodes;
|
||||
private readonly IRepository<StockLedger> _ledger;
|
||||
private readonly IStockMutator _mutator;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
@@ -30,19 +32,62 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
|
||||
public AdjustmentService(
|
||||
IRepository<StockAdjustment> adjustments, IRepository<Warehouse> warehouses, IRepository<Item> items,
|
||||
IRepository<ReasonCode> reasonCodes, IStockMutator mutator, INumberSequenceService numbers,
|
||||
ICurrentUser currentUser, IUnitOfWork uow)
|
||||
IRepository<ReasonCode> reasonCodes, IRepository<StockLedger> ledger, IStockMutator mutator,
|
||||
INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow)
|
||||
{
|
||||
_adjustments = adjustments;
|
||||
_warehouses = warehouses;
|
||||
_items = items;
|
||||
_reasonCodes = reasonCodes;
|
||||
_ledger = ledger;
|
||||
_mutator = mutator;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<AdjustmentSummaryDto>> ListAsync(
|
||||
PageQuery query, int? warehouseId, int? reasonCodeId, CancellationToken ct = default)
|
||||
{
|
||||
var q = _adjustments.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(a => EF.Functions.ILike(a.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (warehouseId is not null) q = q.Where(a => a.WarehouseId == warehouseId);
|
||||
if (reasonCodeId is not null) q = q.Where(a => a.ReasonCodeId == reasonCodeId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(a => a.AdjustmentId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(a => new AdjustmentSummaryDto(
|
||||
a.AdjustmentId, a.DocNo, a.WarehouseId, a.ReasonCodeId, a.Status,
|
||||
a.CreatedBy, a.CreatedAt, a.Lines.Count))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<AdjustmentSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<AdjustmentDto?> GetAsync(int adjustmentId, CancellationToken ct = default)
|
||||
{
|
||||
var adjustment = await _adjustments.Query().AsNoTracking()
|
||||
.Include(a => a.Lines)
|
||||
.FirstOrDefaultAsync(a => a.AdjustmentId == adjustmentId, ct);
|
||||
if (adjustment is null) return null;
|
||||
|
||||
// The ledger reference is polymorphic (docs/10 C.9) — there is no FK to follow,
|
||||
// so the refs this adjustment posted are recovered by source-doc lookup.
|
||||
var ledgerRefs = await _ledger.Query().AsNoTracking()
|
||||
.Where(l => l.SourceDocType == DocumentTypes.Adjustment && l.SourceDocId == adjustmentId)
|
||||
.OrderBy(l => l.LedgerId)
|
||||
.Select(l => l.LedgerId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return ToDto(adjustment, ledgerRefs);
|
||||
}
|
||||
|
||||
public async Task<AdjustmentDto> CreateAsync(CreateAdjustmentRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (request.ReasonCodeId is null)
|
||||
@@ -94,11 +139,12 @@ public sealed class AdjustmentService : IAdjustmentService
|
||||
return (entity, refs);
|
||||
}, ct);
|
||||
|
||||
return new AdjustmentDto(
|
||||
adjustment.AdjustmentId, adjustment.DocNo, adjustment.WarehouseId, adjustment.ReasonCodeId,
|
||||
adjustment.Status, adjustment.CreatedBy, adjustment.CreatedAt,
|
||||
adjustment.Lines.OrderBy(l => l.AdjLineId)
|
||||
.Select(l => new AdjustmentLineDto(l.AdjLineId, l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList(),
|
||||
ledgerRefs.Select(l => l.LedgerId).ToList());
|
||||
return ToDto(adjustment, ledgerRefs.Select(l => l.LedgerId).ToList());
|
||||
}
|
||||
|
||||
private static AdjustmentDto ToDto(StockAdjustment a, IReadOnlyList<int> ledgerRefs) => new(
|
||||
a.AdjustmentId, a.DocNo, a.WarehouseId, a.ReasonCodeId, a.Status, a.CreatedBy, a.CreatedAt,
|
||||
a.Lines.OrderBy(l => l.AdjLineId)
|
||||
.Select(l => new AdjustmentLineDto(l.AdjLineId, l.ItemId, l.BinId, l.BatchId, l.QtyDelta)).ToList(),
|
||||
ledgerRefs);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,11 @@ public sealed class AuthUserService : IAuthUserService
|
||||
public Task<VerifyPasswordResponse> VerifyPasswordAsync(VerifyPasswordRequest request, string bearerToken, CancellationToken ct = default)
|
||||
=> _authHex.VerifyPasswordAsync(request, bearerToken, ct);
|
||||
|
||||
/// <summary>The controller resolves the id (from body or token claim) before calling here.</summary>
|
||||
public Task LogoutUserAsync(LogoutRequest request, CancellationToken ct = default)
|
||||
=> _authHex.LogoutUserAsync(request.UserId, ct);
|
||||
=> request.UserId is null
|
||||
? Task.CompletedTask
|
||||
: _authHex.LogoutUserAsync(request.UserId.Value, ct);
|
||||
|
||||
public Task<UserSummaryDto?> UpdateUserAsync(UpdateUserRequest request, string bearerToken, CancellationToken ct = default)
|
||||
=> _authHex.UpdateUserAsync(request, bearerToken, ct);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
@@ -49,6 +50,30 @@ public sealed class CountService : ICountService
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<CountSummaryDto>> ListAsync(
|
||||
PageQuery query, CountStatus? status, int? warehouseId, CancellationToken ct = default)
|
||||
{
|
||||
var q = _counts.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(c => EF.Functions.ILike(c.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(c => c.Status == status);
|
||||
if (warehouseId is not null) q = q.Where(c => c.WarehouseId == warehouseId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(c => c.CountId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(c => new CountSummaryDto(
|
||||
c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status,
|
||||
c.CreatedBy, c.CreatedAt, c.Lines.Count))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<CountSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<CountDto?> GetAsync(int countId, CancellationToken ct = default)
|
||||
{
|
||||
var count = await _counts.Query().AsNoTracking().Include(c => c.Lines)
|
||||
@@ -175,7 +200,7 @@ public sealed class CountService : ICountService
|
||||
}
|
||||
|
||||
private static CountDto Map(StockCount c) => new(
|
||||
c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status,
|
||||
c.CountId, c.DocNo, c.WarehouseId, c.CountType, c.Status, c.CreatedBy, c.CreatedAt,
|
||||
c.Lines.OrderBy(l => l.CountLineId)
|
||||
.Select(l => new CountLineDto(l.CountLineId, l.ItemId, l.BinId, l.SystemQty, l.CountedQty, l.Variance)).ToList());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Grn;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
@@ -65,6 +66,32 @@ public sealed class GrnService : IGrnService
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<GrnSummaryDto>> ListAsync(
|
||||
PageQuery query, GrnStatus? status, int? poId, int? vendorId, int? warehouseId, CancellationToken ct = default)
|
||||
{
|
||||
var q = _grns.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(g => EF.Functions.ILike(g.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(g => g.Status == status);
|
||||
if (poId is not null) q = q.Where(g => g.PoId == poId);
|
||||
if (vendorId is not null) q = q.Where(g => g.VendorId == vendorId);
|
||||
if (warehouseId is not null) q = q.Where(g => g.WarehouseId == warehouseId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(g => g.GrnId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(g => new GrnSummaryDto(
|
||||
g.GrnId, g.DocNo, g.PoId, g.VendorId, g.WarehouseId, g.Status,
|
||||
g.CreatedBy, g.CreatedAt, g.PostedAt, g.Lines.Count))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<GrnSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<GrnDto?> GetAsync(int grnId, CancellationToken ct = default)
|
||||
{
|
||||
var grn = await _grns.Query().AsNoTracking()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
@@ -5,5 +6,10 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>Stock-adjustment business logic (docs/11 §5.5; FR-STK-07; 02-SECURITY C.5).</summary>
|
||||
public interface IAdjustmentService
|
||||
{
|
||||
Task<PagedResponse<AdjustmentSummaryDto>> ListAsync(
|
||||
PageQuery query, int? warehouseId, int? reasonCodeId, CancellationToken ct = default);
|
||||
|
||||
Task<AdjustmentDto?> GetAsync(int adjustmentId, CancellationToken ct = default);
|
||||
|
||||
Task<AdjustmentDto> CreateAsync(CreateAdjustmentRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
@@ -5,6 +7,9 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>Cycle/full stock-count business logic (docs/11 §5.6; FR-STK-08).</summary>
|
||||
public interface ICountService
|
||||
{
|
||||
Task<PagedResponse<CountSummaryDto>> ListAsync(
|
||||
PageQuery query, CountStatus? status, int? warehouseId, CancellationToken ct = default);
|
||||
|
||||
Task<CountDto?> GetAsync(int countId, CancellationToken ct = default);
|
||||
Task<CountDto> CreateAsync(CreateCountRequest request, CancellationToken ct = default);
|
||||
Task<CountDto> EnterCountsAsync(int countId, EnterCountsRequest request, CancellationToken ct = default);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Grn;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
@@ -5,6 +7,9 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>Goods-receipt business logic (docs/11 §4; FR-GRN-01..08).</summary>
|
||||
public interface IGrnService
|
||||
{
|
||||
Task<PagedResponse<GrnSummaryDto>> ListAsync(
|
||||
PageQuery query, GrnStatus? status, int? poId, int? vendorId, int? warehouseId, CancellationToken ct = default);
|
||||
|
||||
Task<GrnDto?> GetAsync(int grnId, CancellationToken ct = default);
|
||||
Task<GrnDto> CreateAsync(CreateGrnRequest request, CancellationToken ct = default);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
@@ -5,5 +6,10 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>Purchase-return business logic (docs/11 §3.4; FR-PROC-08).</summary>
|
||||
public interface IPurchaseReturnService
|
||||
{
|
||||
Task<PagedResponse<PurchaseReturnSummaryDto>> ListAsync(
|
||||
PageQuery query, int? vendorId, int? warehouseId, CancellationToken ct = default);
|
||||
|
||||
Task<PurchaseReturnDto?> GetAsync(int returnId, CancellationToken ct = default);
|
||||
|
||||
Task<PurchaseReturnDto> CreateAsync(CreatePurchaseReturnRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
|
||||
@@ -6,7 +7,8 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>Purchase-requisition business logic (docs/11 §3.1).</summary>
|
||||
public interface IRequisitionService
|
||||
{
|
||||
Task<PagedResponse<RequisitionSummaryDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||
Task<PagedResponse<RequisitionSummaryDto>> ListAsync(
|
||||
PageQuery query, RequisitionStatus? status, CancellationToken ct = default);
|
||||
Task<RequisitionDto?> GetAsync(int requisitionId, CancellationToken ct = default);
|
||||
Task<RequisitionDto> CreateAsync(CreateRequisitionRequest request, CancellationToken ct = default);
|
||||
Task<RequisitionDto> SubmitAsync(int requisitionId, CancellationToken ct = default);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
@@ -5,6 +7,8 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>RFQ & vendor-quotation business logic (docs/11 §3.2).</summary>
|
||||
public interface IRfqService
|
||||
{
|
||||
Task<PagedResponse<RfqSummaryDto>> ListAsync(PageQuery query, RfqStatus? status, CancellationToken ct = default);
|
||||
|
||||
Task<RfqDto?> GetAsync(int rfqId, CancellationToken ct = default);
|
||||
Task<RfqDto> CreateAsync(CreateRfqRequest request, CancellationToken ct = default);
|
||||
Task<VendorQuotationDto> AddQuotationAsync(int rfqId, CreateQuotationRequest request, CancellationToken ct = default);
|
||||
|
||||
@@ -8,8 +8,13 @@ public interface IStockService
|
||||
{
|
||||
Task<StockOnHandDto> GetOnHandAsync(int itemId, int warehouseId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>On-hand for every (item, warehouse) pair holding stock — backs the enquiry list.</summary>
|
||||
Task<PagedResponse<StockOnHandDto>> GetOnHandListAsync(
|
||||
int? itemId, int? warehouseId, PageQuery query, CancellationToken ct = default);
|
||||
|
||||
Task<PagedResponse<StockLedgerRowDto>> GetLedgerAsync(
|
||||
int? itemId, int? warehouseId, DateOnly? from, DateOnly? to, PageQuery query, CancellationToken ct = default);
|
||||
int? itemId, int? warehouseId, DateOnly? from, DateOnly? to,
|
||||
string? sourceDocType, int? sourceDocId, PageQuery query, CancellationToken ct = default);
|
||||
|
||||
Task<StockValuationDto> GetValuationAsync(int itemId, int warehouseId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
@@ -5,6 +7,9 @@ namespace ERPCore.Services.Interfaces;
|
||||
/// <summary>Inter-warehouse transfer business logic (docs/11 §5.4; FR-STK-05/06).</summary>
|
||||
public interface ITransferService
|
||||
{
|
||||
Task<PagedResponse<TransferSummaryDto>> ListAsync(
|
||||
PageQuery query, TransferStatus? status, int? srcWarehouseId, int? destWarehouseId, CancellationToken ct = default);
|
||||
|
||||
Task<TransferDto?> GetAsync(int transferId, CancellationToken ct = default);
|
||||
Task<TransferDto> CreateAsync(CreateTransferRequest request, CancellationToken ct = default);
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ public sealed class ItemService : IItemService
|
||||
{
|
||||
var item = await _items.Query().AsNoTracking()
|
||||
.Include(i => i.ReorderSettings)
|
||||
.Include(i => i.UomConversions)
|
||||
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct);
|
||||
|
||||
return item is null ? null : new ETagged<ItemDetailDto>(ToDetail(item), item.RowVersion);
|
||||
@@ -131,6 +132,7 @@ public sealed class ItemService : IItemService
|
||||
{
|
||||
var item = await _items.Query()
|
||||
.Include(i => i.ReorderSettings)
|
||||
.Include(i => i.UomConversions)
|
||||
.FirstOrDefaultAsync(i => i.ItemId == itemId, ct)
|
||||
?? throw new NotFoundException($"Item {itemId} was not found.");
|
||||
|
||||
@@ -351,5 +353,9 @@ public sealed class ItemService : IItemService
|
||||
.OrderBy(r => r.WarehouseId)
|
||||
.Select(r => new ItemReorderDto(r.WarehouseId, r.ReorderPoint, r.ReorderQty))
|
||||
.ToList(),
|
||||
i.UomConversions
|
||||
.OrderBy(c => c.ConversionId)
|
||||
.Select(c => new UomConversionDto(c.ConversionId, c.FromUomId, c.ToUomId, c.Factor))
|
||||
.ToList(),
|
||||
i.CreatedAt, i.UpdatedAt);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
@@ -25,6 +26,7 @@ public sealed class PurchaseReturnService : IPurchaseReturnService
|
||||
private readonly IRepository<Item> _items;
|
||||
private readonly IRepository<ReasonCode> _reasonCodes;
|
||||
private readonly IRepository<GrnLine> _grnLines;
|
||||
private readonly IRepository<StockLedger> _ledger;
|
||||
private readonly IStockMutator _mutator;
|
||||
private readonly INumberSequenceService _numbers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
@@ -33,7 +35,8 @@ public sealed class PurchaseReturnService : IPurchaseReturnService
|
||||
public PurchaseReturnService(
|
||||
IRepository<PurchaseReturn> returns, IRepository<Vendor> vendors, IRepository<Warehouse> warehouses,
|
||||
IRepository<Item> items, IRepository<ReasonCode> reasonCodes, IRepository<GrnLine> grnLines,
|
||||
IStockMutator mutator, INumberSequenceService numbers, ICurrentUser currentUser, IUnitOfWork uow)
|
||||
IRepository<StockLedger> ledger, IStockMutator mutator, INumberSequenceService numbers,
|
||||
ICurrentUser currentUser, IUnitOfWork uow)
|
||||
{
|
||||
_returns = returns;
|
||||
_vendors = vendors;
|
||||
@@ -41,12 +44,54 @@ public sealed class PurchaseReturnService : IPurchaseReturnService
|
||||
_items = items;
|
||||
_reasonCodes = reasonCodes;
|
||||
_grnLines = grnLines;
|
||||
_ledger = ledger;
|
||||
_mutator = mutator;
|
||||
_numbers = numbers;
|
||||
_currentUser = currentUser;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<PurchaseReturnSummaryDto>> ListAsync(
|
||||
PageQuery query, int? vendorId, int? warehouseId, CancellationToken ct = default)
|
||||
{
|
||||
var q = _returns.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(r => EF.Functions.ILike(r.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (vendorId is not null) q = q.Where(r => r.VendorId == vendorId);
|
||||
if (warehouseId is not null) q = q.Where(r => r.WarehouseId == warehouseId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(r => r.ReturnId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(r => new PurchaseReturnSummaryDto(
|
||||
r.ReturnId, r.DocNo, r.VendorId, r.WarehouseId, r.ReasonCodeId, r.Status,
|
||||
r.CreatedBy, r.CreatedAt, r.Lines.Count))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<PurchaseReturnSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<PurchaseReturnDto?> GetAsync(int returnId, CancellationToken ct = default)
|
||||
{
|
||||
var ret = await _returns.Query().AsNoTracking()
|
||||
.Include(r => r.Lines)
|
||||
.FirstOrDefaultAsync(r => r.ReturnId == returnId, ct);
|
||||
if (ret is null) return null;
|
||||
|
||||
// Polymorphic ledger reference (docs/10 C.9) — recovered by source-doc lookup.
|
||||
var ledgerRefs = await _ledger.Query().AsNoTracking()
|
||||
.Where(l => l.SourceDocType == DocumentTypes.PurchaseReturn && l.SourceDocId == returnId)
|
||||
.OrderBy(l => l.LedgerId)
|
||||
.Select(l => l.LedgerId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return ToDto(ret, ledgerRefs);
|
||||
}
|
||||
|
||||
public async Task<PurchaseReturnDto> CreateAsync(CreatePurchaseReturnRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (request.ReasonCodeId is null)
|
||||
@@ -105,11 +150,12 @@ public sealed class PurchaseReturnService : IPurchaseReturnService
|
||||
}, ct);
|
||||
|
||||
// Map ledger ids after commit so they are populated.
|
||||
return new PurchaseReturnDto(
|
||||
entity.ReturnId, entity.DocNo, entity.VendorId, entity.WarehouseId, entity.ReasonCodeId, entity.Status,
|
||||
entity.CreatedBy,
|
||||
entity.Lines.OrderBy(l => l.ReturnLineId)
|
||||
.Select(l => new PurchaseReturnLineDto(l.ReturnLineId, l.GrnLineId, l.ItemId, l.Qty)).ToList(),
|
||||
ledgerEntries.Select(r => r.LedgerId).ToList());
|
||||
return ToDto(entity, ledgerEntries.Select(r => r.LedgerId).ToList());
|
||||
}
|
||||
|
||||
private static PurchaseReturnDto ToDto(PurchaseReturn r, IReadOnlyList<int> ledgerRefs) => new(
|
||||
r.ReturnId, r.DocNo, r.VendorId, r.WarehouseId, r.ReasonCodeId, r.Status, r.CreatedBy, r.CreatedAt,
|
||||
r.Lines.OrderBy(l => l.ReturnLineId)
|
||||
.Select(l => new PurchaseReturnLineDto(l.ReturnLineId, l.GrnLineId, l.ItemId, l.Qty)).ToList(),
|
||||
ledgerRefs);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ public sealed class RequisitionService : IRequisitionService
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<RequisitionSummaryDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||
public async Task<PagedResponse<RequisitionSummaryDto>> ListAsync(
|
||||
PageQuery query, RequisitionStatus? status, CancellationToken ct = default)
|
||||
{
|
||||
var q = _requisitions.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
@@ -39,11 +40,13 @@ public sealed class RequisitionService : IRequisitionService
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(r => EF.Functions.ILike(r.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(r => r.Status == status);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(r => r.RequisitionId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(r => new RequisitionSummaryDto(r.RequisitionId, r.DocNo, r.Status, r.RequestedBy, r.CreatedAt))
|
||||
.Select(r => new RequisitionSummaryDto(
|
||||
r.RequisitionId, r.DocNo, r.Status, r.RequestedBy, r.CreatedAt, r.Lines.Count))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<RequisitionSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Procurement;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
@@ -34,6 +35,31 @@ public sealed class RfqService : IRfqService
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<RfqSummaryDto>> ListAsync(
|
||||
PageQuery query, RfqStatus? status, CancellationToken ct = default)
|
||||
{
|
||||
var q = _rfqs.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(r => EF.Functions.ILike(r.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(r => r.Status == status);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(r => r.RfqId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(r => new RfqSummaryDto(
|
||||
r.RfqId, r.DocNo, r.RequisitionId, r.Status,
|
||||
r.Lines.Count,
|
||||
// Correlated subquery: there is no Rfq.Quotations navigation to count.
|
||||
_quotations.Query().Count(qt => qt.RfqId == r.RfqId)))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<RfqSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<RfqDto?> GetAsync(int rfqId, CancellationToken ct = default)
|
||||
{
|
||||
var rfq = await _rfqs.Query().AsNoTracking()
|
||||
|
||||
@@ -50,14 +50,85 @@ public sealed class StockService : IStockService
|
||||
return new StockOnHandDto(itemId, warehouseId, onHand, available, onHold, inTransit, reserved, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On-hand across every (item, warehouse) pair that holds stock — backs the Stock
|
||||
/// Enquiry list. Deliberately set-based: four grouped queries regardless of page size,
|
||||
/// rather than calling <see cref="GetOnHandAsync"/> per row (which would be N+1).
|
||||
/// Pairs are sourced from <c>StockLayer</c>, so an item that never had a receipt in a
|
||||
/// warehouse simply does not appear.
|
||||
/// </summary>
|
||||
public async Task<PagedResponse<StockOnHandDto>> GetOnHandListAsync(
|
||||
int? itemId, int? warehouseId, PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var layers = _layers.Query().AsNoTracking();
|
||||
if (itemId is not null) layers = layers.Where(l => l.ItemId == itemId);
|
||||
if (warehouseId is not null) layers = layers.Where(l => l.WarehouseId == warehouseId);
|
||||
|
||||
var grouped = layers
|
||||
.GroupBy(l => new { l.ItemId, l.WarehouseId })
|
||||
.Select(g => new { g.Key.ItemId, g.Key.WarehouseId, OnHand = g.Sum(x => x.QtyRemaining) });
|
||||
|
||||
var total = await grouped.CountAsync(ct);
|
||||
var page = await grouped
|
||||
.OrderBy(x => x.ItemId).ThenBy(x => x.WarehouseId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (page.Count == 0)
|
||||
return PagedResponse<StockOnHandDto>.Create([], query.Page, query.PageSize, total);
|
||||
|
||||
// Filtering by the page's ids gives a superset (the cross-product of both lists);
|
||||
// the join below narrows it back to the actual pairs.
|
||||
var itemIds = page.Select(p => p.ItemId).Distinct().ToList();
|
||||
var warehouseIds = page.Select(p => p.WarehouseId).Distinct().ToList();
|
||||
|
||||
var onHold = (await _layers.Query().AsNoTracking()
|
||||
.Where(l => itemIds.Contains(l.ItemId) && warehouseIds.Contains(l.WarehouseId)
|
||||
&& l.GrnLine != null && l.GrnLine.HoldStatus == HoldStatus.OnHold)
|
||||
.GroupBy(l => new { l.ItemId, l.WarehouseId })
|
||||
.Select(g => new { g.Key.ItemId, g.Key.WarehouseId, Qty = g.Sum(x => x.QtyRemaining) })
|
||||
.ToListAsync(ct))
|
||||
.ToDictionary(x => (x.ItemId, x.WarehouseId), x => x.Qty);
|
||||
|
||||
var inTransit = (await _transferLines.Query().AsNoTracking()
|
||||
.Where(l => itemIds.Contains(l.ItemId)
|
||||
&& l.Transfer != null
|
||||
&& warehouseIds.Contains(l.Transfer.SrcWarehouseId)
|
||||
&& l.Transfer.Status == TransferStatus.InTransit)
|
||||
.GroupBy(l => new { l.ItemId, WarehouseId = l.Transfer!.SrcWarehouseId })
|
||||
.Select(g => new { g.Key.ItemId, g.Key.WarehouseId, Qty = g.Sum(x => x.Qty - x.QtyReceived) })
|
||||
.ToListAsync(ct))
|
||||
.ToDictionary(x => (x.ItemId, x.WarehouseId), x => x.Qty);
|
||||
|
||||
var asOf = DateTime.UtcNow;
|
||||
var rows = page.Select(p =>
|
||||
{
|
||||
var key = (p.ItemId, p.WarehouseId);
|
||||
var hold = onHold.GetValueOrDefault(key);
|
||||
var transit = inTransit.GetValueOrDefault(key);
|
||||
const decimal reserved = 0m;
|
||||
// Same formula as GetOnHandAsync: in-transit is reported, not re-subtracted.
|
||||
return new StockOnHandDto(
|
||||
p.ItemId, p.WarehouseId, p.OnHand, p.OnHand - hold - reserved, hold, transit, reserved, asOf);
|
||||
}).ToList();
|
||||
|
||||
return PagedResponse<StockOnHandDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<StockLedgerRowDto>> GetLedgerAsync(
|
||||
int? itemId, int? warehouseId, DateOnly? from, DateOnly? to, PageQuery query, CancellationToken ct = default)
|
||||
int? itemId, int? warehouseId, DateOnly? from, DateOnly? to,
|
||||
string? sourceDocType, int? sourceDocId, PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
var q = _ledger.Query().AsNoTracking();
|
||||
if (itemId is not null) q = q.Where(l => l.ItemId == itemId);
|
||||
if (warehouseId is not null) q = q.Where(l => l.WarehouseId == warehouseId);
|
||||
if (from is not null) q = q.Where(l => l.CreatedAt >= from.Value.ToDateTime(TimeOnly.MinValue));
|
||||
if (to is not null) q = q.Where(l => l.CreatedAt < to.Value.AddDays(1).ToDateTime(TimeOnly.MinValue));
|
||||
// Source-doc filter: the ledger references its originating document polymorphically
|
||||
// (docs/10 C.9), so this is the only way to ask "what did document X post?" —
|
||||
// needed by any screen that reports on a document's costed movements.
|
||||
if (!string.IsNullOrWhiteSpace(sourceDocType)) q = q.Where(l => l.SourceDocType == sourceDocType);
|
||||
if (sourceDocId is not null) q = q.Where(l => l.SourceDocId == sourceDocId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(l => l.LedgerId)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Domain;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Stock;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.UoW;
|
||||
@@ -40,6 +41,31 @@ public sealed class TransferService : ITransferService
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<TransferSummaryDto>> ListAsync(
|
||||
PageQuery query, TransferStatus? status, int? srcWarehouseId, int? destWarehouseId, CancellationToken ct = default)
|
||||
{
|
||||
var q = _transfers.Query().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(t => EF.Functions.ILike(t.DocNo, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(t => t.Status == status);
|
||||
if (srcWarehouseId is not null) q = q.Where(t => t.SrcWarehouseId == srcWarehouseId);
|
||||
if (destWarehouseId is not null) q = q.Where(t => t.DestWarehouseId == destWarehouseId);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderByDescending(t => t.TransferId)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.Select(t => new TransferSummaryDto(
|
||||
t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status,
|
||||
t.CreatedBy, t.CreatedAt, t.Lines.Count))
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<TransferSummaryDto>.Create(rows, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<TransferDto?> GetAsync(int transferId, CancellationToken ct = default)
|
||||
{
|
||||
var t = await _transfers.Query().AsNoTracking().Include(x => x.Lines)
|
||||
@@ -190,7 +216,7 @@ public sealed class TransferService : ITransferService
|
||||
}
|
||||
|
||||
private static TransferDto Map(StockTransfer t) => new(
|
||||
t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status,
|
||||
t.TransferId, t.DocNo, t.SrcWarehouseId, t.DestWarehouseId, t.Status, t.CreatedBy, t.CreatedAt,
|
||||
t.Lines.OrderBy(l => l.TransferLineId).Select(l => new TransferLineDto(
|
||||
l.TransferLineId, l.ItemId, l.SrcBinId, l.DestBinId, l.BatchId, l.Qty, l.QtyReceived)).ToList());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user