feat: Add new services and interfaces for GRN, Purchase Return, Reason Code, Reorder, Stock, and Transfer functionalities

- Implemented IGrnService for managing goods receipts including retrieval, creation, and confirmation.
- Created IPurchaseReturnService for handling purchase return operations.
- Added IReasonCodeService for managing reason codes with listing and creation capabilities.
- Developed IReorderService for fetching reorder alerts and creating suggested requisitions.
- Introduced IStockMutator for applying stock changes and posting ledger entries.
- Established IStockService for stock inquiries, ledger retrieval, and valuation.
- Created ITransferService for managing inter-warehouse transfers including dispatch and receiving operations.
- Implemented PurchaseReturnService to handle purchase return logic and stock adjustments.
- Developed ReasonCodeService for listing and creating reason codes.
- Created ReorderService for fetching reorder alerts and generating requisitions.
- Implemented FifoCostingService for FIFO cost-layer management and ledger writing.
- Developed StockMutator for applying stock deltas and posting ledger entries.
- Created StockService for stock inquiries and ledger management.
- Implemented TransferService for managing inter-warehouse transfers with dispatch and receive functionalities.
This commit is contained in:
2026-07-14 10:20:38 +05:30
parent 4e84a15db7
commit 22f86451e3
90 changed files with 13496 additions and 19 deletions
+12
View File
@@ -0,0 +1,12 @@
using System.Text.Json;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Audit;
/// <summary>An audit-trail entry (FR-X-02). <c>ChangeSet</c> is the stored JSON, inlined.</summary>
public sealed record AuditLogDto(
long AuditId, long UserId, string EntityType, long EntityId, AuditAction Action, JsonElement ChangeSet, DateTime CreatedAt);
/// <summary>A GL-ready journal stub emitted per stock movement (FR-STK-13).</summary>
public sealed record JournalEntryStubDto(
long JournalId, string SourceDocType, long SourceDocId, string DebitAccount, string CreditAccount, decimal Amount);
+63
View File
@@ -0,0 +1,63 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Grn;
// Responses (docs/11 §4) --------------------------------------------------------
public sealed record GrnLineDto(
long GrnLineId, long? PoLineId, long ItemId, long UomId, long? BinId,
decimal Qty, decimal UnitCost, decimal ReceivedValue, HoldStatus HoldStatus, long? BatchId);
public sealed record GrnDto(
long GrnId, string DocNo, long? PoId, long VendorId, long WarehouseId, GrnStatus Status,
long CreatedBy, DateTime CreatedAt, DateTime? PostedAt, IReadOnlyList<GrnLineDto> Lines);
public sealed record CreatedLayerDto(
long LayerId, long ItemId, long WarehouseId, long? BatchId,
decimal QtyReceived, decimal QtyRemaining, decimal UnitCost, DateTime ReceiptDate);
public sealed record GrnConfirmResultDto(
long GrnId, GrnStatus Status, DateTime PostedAt,
IReadOnlyList<CreatedLayerDto> CreatedLayers, IReadOnlyList<long> LedgerRefs, PurchaseOrderStatus? PoStatus);
public sealed record ReleaseLineResultDto(long GrnLineId, HoldStatus HoldStatus);
// Requests ----------------------------------------------------------------------
public sealed class BatchInput
{
[Required, StringLength(50)] public string BatchNo { get; set; } = string.Empty;
public DateOnly? ExpiryDate { get; set; }
}
public sealed class CreateGrnLineInput
{
/// <summary>Set for a PO-based receipt; cost is then derived from the PO line (02-SECURITY C.3).</summary>
public long? PoLineId { get; set; }
[Required] public long ItemId { get; set; }
[Required] public long UomId { get; set; }
public long? BinId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
/// <summary>Used only for direct (no-PO) receipts; ignored when <see cref="PoLineId"/> is set.</summary>
[Range(0, double.MaxValue)] public decimal UnitCost { get; set; }
[EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
public BatchInput? Batch { get; set; }
}
public sealed class CreateGrnRequest
{
/// <summary>PO to receive against; null for a direct/emergency receipt (FR-GRN-02).</summary>
public long? PoId { get; set; }
/// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary>
public long? VendorId { get; set; }
[Required] public long WarehouseId { get; set; }
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
}
public sealed class ReleaseLineRequest
{
/// <summary><c>Release</c> makes the stock available; <c>Reject</c> removes it from on-hand.</summary>
[Required, RegularExpression("Release|Reject")]
public string Action { get; set; } = "Release";
}
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Procurement;
// Responses (docs/11 §3.4) ------------------------------------------------------
public sealed record PurchaseReturnLineDto(long ReturnLineId, long? GrnLineId, long ItemId, decimal Qty);
public sealed record PurchaseReturnDto(
long ReturnId, string DocNo, long VendorId, long WarehouseId, long ReasonCodeId, ReturnStatus Status,
long CreatedBy, IReadOnlyList<PurchaseReturnLineDto> Lines, IReadOnlyList<long> LedgerRefs);
// Requests ----------------------------------------------------------------------
public sealed class CreatePurchaseReturnLineInput
{
/// <summary>Original GRN line, for traceability against the receipt.</summary>
public long? GrnLineId { get; set; }
[Required] public long ItemId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
}
public sealed class CreatePurchaseReturnRequest
{
[Required] public long VendorId { get; set; }
[Required] public long WarehouseId { get; set; }
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error.</summary>
public long? ReasonCodeId { get; set; }
[Required, MinLength(1)] public List<CreatePurchaseReturnLineInput> Lines { get; set; } = new();
}
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Reference;
/// <summary>Reason code (docs/11 §6).</summary>
public sealed record ReasonCodeDto(long ReasonCodeId, string Code, string Description, ReasonContext Context);
public sealed class CreateReasonCodeRequest
{
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
[Required, StringLength(200)] public string Description { get; set; } = string.Empty;
[Required, EnumDataType(typeof(ReasonContext))] public ReasonContext Context { get; set; }
}
@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Stock;
// Responses (docs/11 §5.5) ------------------------------------------------------
public sealed record AdjustmentLineDto(long AdjLineId, long ItemId, long? BinId, long? BatchId, decimal QtyDelta);
public sealed record AdjustmentDto(
long AdjustmentId, string DocNo, long WarehouseId, long ReasonCodeId, AdjustmentStatus Status,
long CreatedBy, DateTime CreatedAt, IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<long> LedgerRefs);
// Requests ----------------------------------------------------------------------
public sealed class CreateAdjustmentLineInput
{
[Required] public long ItemId { get; set; }
public long? BinId { get; set; }
public long? BatchId { get; set; }
/// <summary>Signed base-UOM delta: negative consumes FIFO layers, positive adds stock.</summary>
public decimal QtyDelta { get; set; }
}
public sealed class CreateAdjustmentRequest
{
[Required] public long WarehouseId { get; set; }
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error, not 0.</summary>
public long? ReasonCodeId { get; set; }
[Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new();
}
+33
View File
@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Stock;
// Responses (docs/11 §5.6) ------------------------------------------------------
public sealed record CountLineDto(long CountLineId, long ItemId, long? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
public sealed record CountDto(
long CountId, string DocNo, long WarehouseId, CountType CountType, CountStatus Status, IReadOnlyList<CountLineDto> Lines);
public sealed record CountPostResultDto(long CountId, CountStatus Status, long? AdjustmentId, IReadOnlyList<long> LedgerRefs);
// Requests ----------------------------------------------------------------------
public sealed class CreateCountRequest
{
[Required] public long WarehouseId { get; set; }
[Required, EnumDataType(typeof(CountType))] public CountType CountType { get; set; }
[Required, MinLength(1)] public List<long> ItemIds { get; set; } = new();
}
public sealed class EnterCountLineInput
{
[Required] public long CountLineId { get; set; }
[Range(0, double.MaxValue)] public decimal CountedQty { get; set; }
}
public sealed class EnterCountsRequest
{
[Required, MinLength(1)] public List<EnterCountLineInput> Lines { get; set; } = new();
}
@@ -0,0 +1,6 @@
namespace ERPCore.Dtos.Stock;
/// <summary>An item at/below its reorder point (docs/11 §5.7; FR-STK-10). Computed on read.</summary>
public sealed record ReorderAlertDto(
long ItemId, long WarehouseId, decimal Available,
decimal ReorderPoint, decimal ReorderQty, decimal SuggestedRequisitionQty);
+22
View File
@@ -0,0 +1,22 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Stock;
/// <summary>Stock enquiry (docs/11 §5.1). available = onHand onHold reserved inTransit(out).</summary>
public sealed record StockOnHandDto(
long ItemId, long WarehouseId, decimal OnHand, decimal Available,
decimal OnHold, decimal InTransit, decimal Reserved, DateTime AsOf);
/// <summary>A stock-ledger row (docs/11 §5.2).</summary>
public sealed record StockLedgerRowDto(
long LedgerId, long ItemId, long WarehouseId, long? BinId, long? BatchId, long? SerialId,
Direction Direction, decimal QtyBase, decimal UnitCost, decimal Value, decimal RunningBalance,
string SourceDocType, long SourceDocId, long UserId, DateTime CreatedAt);
/// <summary>An open FIFO layer in a valuation (docs/11 §5.3).</summary>
public sealed record StockValuationLayerDto(long LayerId, decimal QtyRemaining, decimal UnitCost, decimal Value, DateTime ReceiptDate);
/// <summary>Valuation of on-hand stock from open FIFO layers (docs/11 §5.3).</summary>
public sealed record StockValuationDto(
long ItemId, long WarehouseId, IReadOnlyList<StockValuationLayerDto> Layers,
decimal TotalQty, decimal TotalValue, string Currency, string CostingMethod);
@@ -0,0 +1,52 @@
using System.ComponentModel.DataAnnotations;
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Stock;
// Responses (docs/11 §5.4) ------------------------------------------------------
public sealed record TransferLineDto(
long TransferLineId, long ItemId, long? SrcBinId, long? DestBinId, long? BatchId, decimal Qty, decimal QtyReceived);
public sealed record TransferDto(
long TransferId, string DocNo, long SrcWarehouseId, long DestWarehouseId,
TransferStatus Status, IReadOnlyList<TransferLineDto> Lines);
public sealed record ConsumedLayerDto(long LayerId, decimal QtyConsumed, decimal UnitCost);
public sealed record DispatchResultDto(
long TransferId, TransferStatus Status, IReadOnlyList<ConsumedLayerDto> ConsumedLayers, IReadOnlyList<long> LedgerRefs);
public sealed record TransferCreatedLayerDto(long LayerId, long WarehouseId, decimal QtyReceived, decimal UnitCost);
public sealed record ReceiveResultDto(
long TransferId, TransferStatus Status, IReadOnlyList<TransferCreatedLayerDto> CreatedLayers, IReadOnlyList<long> LedgerRefs);
// Requests ----------------------------------------------------------------------
public sealed class CreateTransferLineInput
{
[Required] public long ItemId { get; set; }
public long? SrcBinId { get; set; }
public long? DestBinId { get; set; }
public long? BatchId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
}
public sealed class CreateTransferRequest
{
[Required] public long SrcWarehouseId { get; set; }
[Required] public long DestWarehouseId { get; set; }
[Required, MinLength(1)] public List<CreateTransferLineInput> Lines { get; set; } = new();
}
public sealed class ReceiveTransferLineInput
{
[Required] public long TransferLineId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
}
public sealed class ReceiveTransferRequest
{
[Required, MinLength(1)] public List<ReceiveTransferLineInput> Lines { get; set; } = new();
}