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:
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user