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
@@ -65,4 +65,12 @@ public class GrnLine
/// Empty for a non-warranty item.
/// </summary>
public ICollection<GrnLineWarrantyNumber> WarrantyNumbers { get; set; } = new List<GrnLineWarrantyNumber>();
/// <summary>
/// One row per received unit when <see cref="Item.TrackingMode"/> is
/// <see cref="TrackingMode.Serial"/> — count must equal <see cref="Qty"/>. For a serial item
/// under warranty the serial is the warranty identity, so it carries no
/// <see cref="WarrantyNumbers"/>. Empty otherwise.
/// </summary>
public ICollection<GrnLineSerial> Serials { get; set; } = new List<GrnLineSerial>();
}
@@ -0,0 +1,22 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// A serial number captured on a Draft GRN line for a serial-tracked item
/// (<see cref="Item.TrackingMode"/> = <see cref="Enums.TrackingMode.Serial"/>). The line must
/// carry exactly <see cref="GrnLine.Qty"/> of these. The draft holds the numbers only — the
/// <see cref="Serial"/> rows (and their qty-1 FIFO layers) are created when the GRN is
/// confirmed, so a draft that is never confirmed reserves nothing (docs/ITEM_TRACKING_PLAN.md Phase 2).
/// </summary>
public class GrnLineSerial
{
public int GrnLineSerialId { get; set; }
public int GrnLineId { get; set; }
public GrnLine? GrnLine { get; set; }
public string SerialNo { get; set; } = string.Empty;
/// <summary>Set on confirm — the <see cref="Serial"/> created for this number.</summary>
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
}
@@ -19,6 +19,13 @@ public class SalesInvoiceLine
public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }
/// <summary>Pins the line to one batch; null lets posting pick batches FEFO.</summary>
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
/// <summary>Chosen serials for a serial-tracked item (one per unit of Qty + FreeQty).</summary>
public ICollection<SalesInvoiceLineSerial> Serials { get; set; } = new List<SalesInvoiceLineSerial>();
public decimal UnitPrice { get; set; }
public decimal BaseCost { get; set; }
public string PriceSource { get; set; } = string.Empty;
@@ -0,0 +1,35 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// What a posted sales line actually issued: one row per consumed FIFO segment, carrying the
/// batch (FEFO-picked when the line named none) and, for serial items, the serial. The source
/// line is referenced polymorphically (<see cref="SourceDocType"/> is a
/// <see cref="Domain.DocumentTypes"/> value) — same convention as <see cref="StockLedger"/> —
/// so invoices, slips and bundles share one table. Returns read it to restock into the same
/// batch/serial (docs/ITEM_TRACKING_PLAN.md Phases 4 and 6).
/// </summary>
public class SalesLineAllocation
{
public int SalesLineAllocationId { get; set; }
public string SourceDocType { get; set; } = string.Empty;
public int SourceDocId { get; set; }
public int SourceLineId { get; set; }
public int ItemId { get; set; }
public int WarehouseId { get; set; }
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
public decimal Qty { get; set; }
public decimal UnitCost { get; set; }
/// <summary>Quantity of this allocation already taken back by sales returns.</summary>
public decimal QtyReturned { get; set; }
public DateTime CreatedAt { get; set; }
}
@@ -0,0 +1,29 @@
namespace ERPCore.Domain.Entities;
/// <summary>
/// A serial chosen on a Draft sales-invoice line for a serial-tracked item — one row per unit
/// (count = Qty + FreeQty). Choosing does not reserve: posting re-checks each serial is still
/// in stock and consumes exactly its layer (docs/ITEM_TRACKING_PLAN.md Phase 4, D4).
/// </summary>
public class SalesInvoiceLineSerial
{
public int SalesInvoiceLineSerialId { get; set; }
public int SalesInvoiceLineId { get; set; }
public SalesInvoiceLine? SalesInvoiceLine { get; set; }
public int SerialId { get; set; }
public Serial? Serial { get; set; }
}
/// <summary>Sales-slip counterpart of <see cref="SalesInvoiceLineSerial"/>.</summary>
public class SalesSlipLineSerial
{
public int SalesSlipLineSerialId { get; set; }
public int SalesSlipLineId { get; set; }
public SalesSlipLine? SalesSlipLine { get; set; }
public int SerialId { get; set; }
public Serial? Serial { get; set; }
}
@@ -2,7 +2,9 @@ namespace ERPCore.Domain.Entities;
/// <summary>
/// Sales-return line referencing the original sales invoice line for traceability.
/// <see cref="Qty"/> is in base UOM.
/// <see cref="Qty"/> is in base UOM. <see cref="BatchId"/> is the batch the goods are restocked
/// into — the batch they were sold from (docs/ITEM_TRACKING_PLAN.md Phase 6); <see cref="Serials"/>
/// lists the returned units of a serial-tracked item.
/// </summary>
public class SalesReturnLine
{
@@ -18,4 +20,21 @@ public class SalesReturnLine
public Item? Item { get; set; }
public decimal Qty { get; set; }
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
public ICollection<SalesReturnLineSerial> Serials { get; set; } = new List<SalesReturnLineSerial>();
}
/// <summary>One serial taken back on a <see cref="SalesReturnLine"/>.</summary>
public class SalesReturnLineSerial
{
public int SalesReturnLineSerialId { get; set; }
public int ReturnLineId { get; set; }
public SalesReturnLine? ReturnLine { get; set; }
public int SerialId { get; set; }
public Serial? Serial { get; set; }
}
@@ -22,9 +22,16 @@ public class SalesSlipLine
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
/// <summary>
/// Legacy single-serial pin, from before a line could carry several. Kept for old drafts and
/// still set when exactly one serial is chosen; <see cref="Serials"/> is authoritative.
/// </summary>
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
/// <summary>Chosen serials for a serial-tracked item (one per unit of Qty + FreeQty).</summary>
public ICollection<SalesSlipLineSerial> Serials { get; set; } = new List<SalesSlipLineSerial>();
public decimal UnitPrice { get; set; }
public decimal BaseCost { get; set; }
public string PriceSource { get; set; } = string.Empty;
+32 -2
View File
@@ -1,8 +1,13 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// Serial number for a serial-tracked item across its lifecycle (FR-WH-04).
/// Model: docs/10 Part C.4.
/// Created when the GRN that received it is confirmed; each in-stock serial owns exactly one
/// FIFO layer of qty 1 (<see cref="StockLayer.SerialId"/>), so issuing a serial consumes that
/// layer. <see cref="WarehouseId"/>/<see cref="BinId"/> are its current location and are
/// cleared once it leaves stock. Model: docs/10 Part C.4, docs/ITEM_TRACKING_PLAN.md Phase 1.
/// </summary>
public class Serial
{
@@ -12,8 +17,33 @@ public class Serial
public Item? Item { get; set; }
public string SerialNo { get; set; } = string.Empty;
public string Status { get; set; } = "InStock";
public SerialStatus Status { get; set; } = SerialStatus.InStock;
/// <summary>Optional scan code for barcode/RFID lookup (unique when set).</summary>
public string? Barcode { get; set; }
/// <summary>Current warehouse while in stock; null once sold/returned to vendor/scrapped.</summary>
public int? WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }
public int? BinId { get; set; }
public Bin? Bin { get; set; }
/// <summary>The GRN line that received this unit (provenance); null for count/adjustment finds.</summary>
public int? GrnLineId { get; set; }
public GrnLine? GrnLine { get; set; }
public DateTime? ReceivedAt { get; set; }
/// <summary>
/// Warranty coverage snapshotted from <see cref="Item.WarrantyPeriodMonths"/> at receipt
/// (same rule as <see cref="GrnLineWarrantyNumber.WarrantyPeriodMonths"/>); null for a
/// non-warranty item. The clock does not start here — it starts at sale
/// (<see cref="WarrantyRegistration"/>).
/// </summary>
public int? WarrantyPeriodMonths { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public uint RowVersion { get; set; }
}
@@ -0,0 +1,46 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// Append-only history of one serial: a row each time a document changes its status or
/// location (receipt, sale, return, transfer, adjustment, count). <see cref="Status"/> and
/// <see cref="WarehouseId"/> are the serial's state <em>after</em> the movement. The source
/// document is referenced polymorphically, as on <see cref="StockLedger"/>. This is where serial
/// history is read from — the stock ledger keeps one row per document line, not per unit
/// (docs/ITEM_TRACKING_PLAN.md Phases 2 and 9).
/// </summary>
public class SerialMovement
{
public int SerialMovementId { get; set; }
public int SerialId { get; set; }
public Serial? Serial { get; set; }
public string SourceDocType { get; set; } = string.Empty;
public int SourceDocId { get; set; }
public SerialStatus Status { get; set; }
public int? FromWarehouseId { get; set; }
public int? WarehouseId { get; set; }
public int UserId { get; set; }
public DateTime CreatedAt { get; set; }
/// <summary>
/// Records <paramref name="serial"/>'s current state — call after updating it. Linked by
/// navigation, so it works for a serial inserted in the same save.
/// </summary>
public static SerialMovement Of(Serial serial, int? fromWarehouseId, string sourceDocType, int sourceDocId, int userId, DateTime at) => new()
{
Serial = serial,
SerialId = serial.SerialId,
SourceDocType = sourceDocType,
SourceDocId = sourceDocId,
Status = serial.Status,
FromWarehouseId = fromWarehouseId,
WarehouseId = serial.WarehouseId,
UserId = userId,
CreatedAt = at
};
}
@@ -18,6 +18,7 @@ public class StockAdjustmentLine
public int? BinId { get; set; }
public int? BatchId { get; set; }
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
public decimal QtyDelta { get; set; }
}
@@ -32,4 +32,27 @@ public class StockTransferLine
/// <summary>Quantity already received at the destination (partial-receive support).</summary>
public decimal QtyReceived { get; set; }
/// <summary>The units moved, for a serial-tracked item (one per unit of <see cref="Qty"/>).</summary>
public ICollection<StockTransferLineSerial> Serials { get; set; } = new List<StockTransferLineSerial>();
}
/// <summary>
/// One serial on a transfer line. <see cref="UnitCost"/>/<see cref="BatchId"/> are captured from
/// its own source layer at dispatch, so the destination layer is recreated exactly;
/// <see cref="ReceivedAt"/> is set when it arrives (partial receive names which serials).
/// </summary>
public class StockTransferLineSerial
{
public int StockTransferLineSerialId { get; set; }
public int TransferLineId { get; set; }
public StockTransferLine? TransferLine { get; set; }
public int SerialId { get; set; }
public Serial? Serial { get; set; }
public int? BatchId { get; set; }
public decimal? UnitCost { get; set; }
public DateTime? ReceivedAt { get; set; }
}
@@ -0,0 +1,42 @@
using ERPCore.Domain.Enums;
namespace ERPCore.Domain.Entities;
/// <summary>
/// A customer's warranty, started when the sale that issued the goods is posted
/// (docs/ITEM_TRACKING_PLAN.md Phase 5, D3/D7) — stock waiting in a warehouse never consumes
/// warranty. For a serial item there is one row per serial (<see cref="SerialId"/> set,
/// <see cref="Qty"/> = 1); for a warranty item that is not serial-tracked there is one row per
/// sales line covering its <see cref="Qty"/>. <see cref="PeriodMonths"/> is the coverage
/// snapshotted at receipt (<see cref="Serial.WarrantyPeriodMonths"/>), else the item's period.
/// </summary>
public class WarrantyRegistration
{
public int WarrantyRegistrationId { get; set; }
public int ItemId { get; set; }
public Item? Item { get; set; }
public int? SerialId { get; set; }
public Serial? Serial { get; set; }
public int CustomerId { get; set; }
public Customer? Customer { get; set; }
public string SourceDocType { get; set; } = string.Empty;
public int SourceDocId { get; set; }
public int SourceLineId { get; set; }
public decimal Qty { get; set; }
public DateOnly StartDate { get; set; }
public int PeriodMonths { get; set; }
public DateOnly EndDate { get; set; }
public WarrantyStatus Status { get; set; } = WarrantyStatus.Active;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public uint RowVersion { get; set; }
}
@@ -0,0 +1,25 @@
namespace ERPCore.Domain.Enums;
/// <summary>
/// Lifecycle state of one serial-tracked unit (FR-WH-04). Stored as a string — the values
/// <c>InStock</c>/<c>Sold</c> written before this enum existed stay valid as-is.
/// Only <see cref="InStock"/> is issuable.
/// </summary>
public enum SerialStatus
{
InStock,
Reserved,
/// <summary>Dispatched on a stock transfer, not yet received — in no warehouse.</summary>
InTransit,
Sold,
ReturnedByCustomer,
ReturnedToVendor,
Damaged,
Scrapped,
/// <summary>
/// Left stock without being named, while serial numbers were switched off in Product
/// Configuration — the unit was issued oldest-first like untracked stock, so which
/// document took it is not recorded against the serial.
/// </summary>
IssuedUntracked
}
@@ -0,0 +1,10 @@
namespace ERPCore.Domain.Enums;
/// <summary>State of a <see cref="Entities.WarrantyRegistration"/>. Stored as a string.</summary>
public enum WarrantyStatus
{
Active,
/// <summary>The unit came back on a sales return — the warranty no longer applies.</summary>
Voided,
Claimed
}