serial numbers
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user