serial numbers

This commit is contained in:
2026-10-07 14:00:32 +05:30
committed by ImanThiyanga
parent 3abcfa6e6f
commit a788349138
89 changed files with 15320 additions and 216 deletions
+12 -2
View File
@@ -7,12 +7,16 @@ namespace ERPCore.Dtos.Grn;
public sealed record GrnLineWarrantyNumberDto(string WarrantyNo, int WarrantyPeriodMonths);
/// <summary>A serial captured on a GRN line; <see cref="SerialId"/> is set once the GRN is confirmed.</summary>
public sealed record GrnLineSerialDto(string SerialNo, int? SerialId);
public sealed record GrnLineDto(
int GrnLineId, int? PoLineId, int ItemId, int? BinId,
decimal Qty, decimal UnitCost, decimal? PoUnitPrice,
decimal DiscountPct, decimal NetUnitCost, decimal VatPct, decimal VatAmount,
decimal ReceivedValue, decimal LineTotal, decimal PriceVariance,
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers,
string? BatchNo, DateOnly? BatchExpiryDate, IReadOnlyList<GrnLineSerialDto> Serials);
public sealed record GrnDto(
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
@@ -70,9 +74,15 @@ public sealed class CreateGrnLineInput
public BatchInput? Batch { get; set; }
/// <summary>
/// Required, one per unit (count must equal <see cref="Qty"/>), when the item is
/// warranty-tracked (<c>Item.Warranty == Warranty.Warranty</c>). Ignored otherwise.
/// warranty-tracked (<c>Item.Warranty == Warranty.Warranty</c>) and not serial-tracked —
/// a serial item's serial is its warranty identity. Ignored otherwise.
/// </summary>
public List<string>? WarrantyNumbers { get; set; }
/// <summary>
/// Required, one per unit (count must equal <see cref="Qty"/>, so Qty must be whole), when the
/// item is serial-tracked (<c>Item.TrackingMode == Serial</c>). Rejected for any other item.
/// </summary>
public List<string>? SerialNumbers { get; set; }
}
public sealed class CreateGrnRequest
@@ -25,6 +25,12 @@ public sealed class CreatePurchaseReturnLineInput
public int? GrnLineId { get; set; }
[Required] public int ItemId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
/// <summary>
/// Serials sent back, for a serial-tracked item — one per unit, each at the return warehouse
/// (InStock or Damaged; for a rejected GRN line, received on that line). They become
/// <c>ReturnedToVendor</c>. Rejected for any other item.
/// </summary>
public List<int>? SerialIds { get; set; }
}
public sealed class CreatePurchaseReturnRequest
+12 -1
View File
@@ -7,7 +7,11 @@ public sealed record SalesInvoiceLineDto(
int SalesInvoiceLineId, int ItemId, string Description, decimal Qty, decimal FreeQty,
int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct,
decimal DiscountAmount, SalesDiscountMode DiscountMode, decimal NetUnitPrice, decimal LineTotal,
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId);
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId,
int? BatchId, IReadOnlyList<SalesLineSerialDto> Serials);
/// <summary>A serial chosen on (or sold by) a sales line.</summary>
public sealed record SalesLineSerialDto(int SerialId, string SerialNo);
public sealed record SalesInvoiceTotalsDto(
decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
@@ -95,6 +99,13 @@ public sealed class CreateSalesInvoiceLineRequest
[Range(0, 100)] public decimal? TaxPct { get; set; }
public bool IsFreeIssue { get; set; }
public int? ParentLineId { get; set; }
/// <summary>Pins the line to one batch (must belong to the item, not expired). Omit to let posting pick FEFO.</summary>
public int? BatchId { get; set; }
/// <summary>
/// Serials sold on this line — required for a serial-tracked item, one per unit of
/// Qty + FreeQty, each InStock at the line warehouse. Rejected for any other item.
/// </summary>
public List<int>? SerialIds { get; set; }
}
public sealed class CreateSalesInvoiceRequest
+28 -2
View File
@@ -5,7 +5,17 @@ namespace ERPCore.Dtos.Sales;
// Responses -----------------------------------------------------------------
public sealed record SalesReturnLineDto(int ReturnLineId, int? SalesInvoiceLineId, int ItemId, decimal Qty);
public sealed record SalesReturnLineDto(
int ReturnLineId, int? SalesInvoiceLineId, int ItemId, decimal Qty,
int? BatchId, IReadOnlyList<SalesLineSerialDto> Serials);
/// <summary>
/// What one posted sales invoice line issued, net of returns — the batches/serials a return
/// against it can take back.
/// </summary>
public sealed record SalesLineAllocationDto(
int? BatchId, string? BatchNo, DateOnly? ExpiryDate, int? SerialId, string? SerialNo,
decimal Qty, decimal QtyReturned);
public sealed record SalesReturnDto(
int ReturnId, string DocNo, int CustomerId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
@@ -22,7 +32,8 @@ public sealed record SalesReturnSummaryDto(
/// whatever has already been returned against it. The invoice line's own <c>Qty</c>
/// is never mutated by a return, so this is computed on read from return history.
/// </summary>
public sealed record SalesInvoiceLineRemainingDto(int SalesInvoiceLineId, decimal RemainingQty);
public sealed record SalesInvoiceLineRemainingDto(
int SalesInvoiceLineId, decimal RemainingQty, IReadOnlyList<SalesLineAllocationDto> Allocations);
// Requests --------------------------------------------------------------------
@@ -32,6 +43,16 @@ public sealed class CreateSalesReturnLineInput
public int? SalesInvoiceLineId { get; set; }
[Required] public int ItemId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
/// <summary>
/// Batch to restock into. Optional when the invoice line issued a single batch (it is used);
/// required for a batch-tracked item otherwise, and must be one the line issued.
/// </summary>
public int? BatchId { get; set; }
/// <summary>
/// Returned serials of a serial-tracked item — one per unit, each Sold (and, with an invoice
/// line, sold on that line). Rejected for any other item.
/// </summary>
public List<int>? SerialIds { get; set; }
}
public sealed class CreateSalesReturnRequest
@@ -40,5 +61,10 @@ public sealed class CreateSalesReturnRequest
[Required] public int WarehouseId { get; set; }
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error.</summary>
public int? ReasonCodeId { get; set; }
/// <summary>
/// Returned serials become <c>Damaged</c> (not resaleable) instead of <c>InStock</c>. Omit to
/// derive it from the reason: <c>DEF</c> (Defective) and <c>QREJ</c> (Quality Reject) mean damaged.
/// </summary>
public bool? MarkDamaged { get; set; }
[Required, MinLength(1)] public List<CreateSalesReturnLineInput> Lines { get; set; } = new();
}
+7 -2
View File
@@ -8,7 +8,7 @@ public sealed record SalesSlipLineDto(
int WarehouseId, decimal UnitPrice, decimal BaseCost, string PriceSource, decimal DiscountPct,
decimal DiscountAmount, SalesDiscountMode DiscountMode, decimal NetUnitPrice, decimal LineTotal,
decimal TaxPct, decimal TaxAmount, bool IsFreeIssue, int? ParentLineId,
int? BatchId, int? SerialId);
int? BatchId, int? SerialId, IReadOnlyList<SalesLineSerialDto> Serials);
public sealed record SalesSlipTotalsDto(
decimal Subtotal, decimal DiscountTotal, decimal FreeQtyTotal, decimal TaxTotal, decimal GrandTotal,
@@ -62,8 +62,13 @@ public sealed class CreateSalesSlipLineRequest
public int? ParentLineId { get; set; }
/// <summary>Pins the line to a specific batch (must belong to <see cref="ItemId"/>), consumed via FIFO.</summary>
public int? BatchId { get; set; }
/// <summary>Pins the line to a specific serial (must belong to <see cref="ItemId"/>); marked Sold on posting.</summary>
/// <summary>Legacy single-serial pin — treated as <c>SerialIds = [SerialId]</c> when <see cref="SerialIds"/> is empty.</summary>
public int? SerialId { get; set; }
/// <summary>
/// Serials sold on this line — required for a serial-tracked item, one per unit of
/// Qty + FreeQty, each InStock at the line warehouse. Marked Sold on posting.
/// </summary>
public List<int>? SerialIds { get; set; }
}
public sealed class CreateSalesSlipRequest
+11 -1
View File
@@ -5,7 +5,7 @@ namespace ERPCore.Dtos.Stock;
// Responses (docs/11 §5.5) ------------------------------------------------------
public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, int? BatchId, decimal QtyDelta);
public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, int? BatchId, decimal QtyDelta, int? SerialId);
public sealed record AdjustmentDto(
int AdjustmentId, string DocNo, int WarehouseId, int BranchId, int ReasonCodeId, AdjustmentStatus Status,
@@ -26,6 +26,16 @@ public sealed class CreateAdjustmentLineInput
public int? BatchId { get; set; }
/// <summary>Signed base-UOM delta: negative consumes FIFO layers, positive adds stock.</summary>
public decimal QtyDelta { get; set; }
/// <summary>
/// Negative delta on a serial-tracked item: the serials written off (InStock or Damaged at
/// the warehouse, one per unit) — they become <c>Scrapped</c>.
/// </summary>
public List<int>? SerialIds { get; set; }
/// <summary>
/// Positive delta on a serial-tracked item: the serial numbers found (one per unit) — new
/// numbers are created, a known serial that is out of stock is brought back InStock.
/// </summary>
public List<string>? SerialNos { get; set; }
}
public sealed class CreateAdjustmentRequest
@@ -0,0 +1,40 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Dtos.Stock;
// Item tracking lookups and registers (docs/ITEM_TRACKING_PLAN.md Phases 5, 8, 9).
/// <summary>Product Configuration tracking switches, as every screen needs them.</summary>
public sealed record TrackingSettingsDto(bool SerialNumbersEnabled);
/// <summary>An issuable serial, for sale/transfer/return pickers.</summary>
public sealed record AvailableSerialDto(int SerialId, string SerialNo, int? BatchId, string? BatchNo, SerialStatus Status);
/// <summary>Issuable stock of one batch at a warehouse (BatchId null = unbatched stock).</summary>
public sealed record AvailableBatchDto(int? BatchId, string? BatchNo, DateOnly? ExpiryDate, decimal Qty, int? DaysToExpiry);
/// <summary>A FEFO pick: <see cref="Qty"/> of <see cref="BatchId"/>.</summary>
public sealed record BatchSuggestionDto(int? BatchId, string? BatchNo, DateOnly? ExpiryDate, decimal Qty);
public sealed record SerialSummaryDto(
int SerialId, int ItemId, string Sku, string ItemName, string SerialNo, SerialStatus Status,
int? WarehouseId, string? WarehouseName, int? BinId, DateTime? ReceivedAt,
int? GrnId, string? GrnDocNo, int? WarrantyPeriodMonths);
public sealed record SerialMovementDto(
int SerialMovementId, string SourceDocType, int SourceDocId, SerialStatus Status,
int? FromWarehouseId, int? WarehouseId, int UserId, DateTime CreatedAt);
public sealed record SerialDetailDto(
SerialSummaryDto Serial, IReadOnlyList<SerialMovementDto> History, IReadOnlyList<WarrantyRegistrationDto> Warranties);
public sealed record WarrantyRegistrationDto(
int WarrantyRegistrationId, int ItemId, string Sku, string ItemName, int? SerialId, string? SerialNo,
int CustomerId, string CustomerName, string SourceDocType, int SourceDocId, int SourceLineId,
decimal Qty, DateOnly StartDate, int PeriodMonths, DateOnly EndDate, WarrantyStatus Status,
int DaysRemaining, bool IsExpired);
/// <summary>Stock on hand per batch (with expiry), for the batch / expiry report.</summary>
public sealed record BatchStockDto(
int ItemId, string Sku, string ItemName, int WarehouseId, string WarehouseName,
int? BatchId, string? BatchNo, DateOnly? ExpiryDate, decimal Qty, int? DaysToExpiry, bool IsExpired);
+13 -1
View File
@@ -6,7 +6,11 @@ namespace ERPCore.Dtos.Stock;
// Responses (docs/11 §5.4) ------------------------------------------------------
public sealed record TransferLineDto(
int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived);
int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived,
IReadOnlyList<TransferLineSerialDto> Serials);
/// <summary>A serial on a transfer line; <see cref="Received"/> once it has arrived.</summary>
public sealed record TransferLineSerialDto(int SerialId, string SerialNo, bool Received);
public sealed record TransferDto(
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int BranchId,
@@ -34,8 +38,11 @@ public sealed class CreateTransferLineInput
[Required] public int ItemId { get; set; }
public int? SrcBinId { get; set; }
public int? DestBinId { get; set; }
/// <summary>Batch to move. Omitted for a batch-tracked item, the line is split FEFO across batches.</summary>
public int? BatchId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
/// <summary>Serials to move — required for a serial-tracked item, one per unit, each InStock at the source.</summary>
public List<int>? SerialIds { get; set; }
}
public sealed class CreateTransferRequest
@@ -52,6 +59,11 @@ public sealed class ReceiveTransferLineInput
{
[Required] public int TransferLineId { get; set; }
[Range(0.0001, double.MaxValue)] public decimal Qty { get; set; }
/// <summary>
/// For a serial line: which in-transit serials arrived (count = Qty). May be omitted when
/// receiving everything still outstanding on the line.
/// </summary>
public List<int>? SerialIds { get; set; }
}
public sealed class ReceiveTransferRequest