Add smoke tests for UOM conversions and enhance frontend UOM management

- Implemented smoke tests for UOM directionality, ensuring conversions are one-directional and correctly validated.
- Added tests for receiving and selling items in different UOMs, verifying correct quantity handling and error responses.
- Created a UOM conversions panel in the frontend to allow users to manage UOM conversions for items.
- Introduced hooks for allowed UOMs to optimize fetching and caching of UOM data for document line forms.
- Developed utility functions for consistent UOM formatting and conversion handling across the application.
This commit is contained in:
2026-08-10 15:12:51 +05:30
parent d37824cecc
commit 8d5a05a419
63 changed files with 1539 additions and 136 deletions
@@ -11,9 +11,20 @@ public class BundleSaleLine
public int ItemId { get; set; }
public Item? Item { get; set; }
public string Description { get; set; } = string.Empty;
/// <summary>Component quantity, in <see cref="UomId"/> — what the user entered.</summary>
public decimal Qty { get; set; }
public int UomId { get; set; }
public Uom? Uom { get; set; }
/// <summary>
/// <see cref="Qty"/> restated in the item's base UOM, resolved once at save. Posting
/// consumes this. <see cref="Qty"/> and <see cref="UnitPrice"/> are always a matching
/// pair in <see cref="UomId"/>, so <see cref="LineTotal"/> stays value-correct.
/// </summary>
public decimal QtyBase { get; set; }
/// <summary>The factor used to derive <see cref="QtyBase"/>; 1 when the line is in base UOM.</summary>
public decimal ConversionFactor { get; set; } = 1m;
public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }
public decimal UnitPrice { get; set; }
@@ -34,8 +34,20 @@ public class GrnLine
public int? BatchId { get; set; }
public Batch? Batch { get; set; }
/// <summary>Quantity received, in <see cref="UomId"/> — what the user entered.</summary>
public decimal Qty { get; set; }
/// <summary>
/// <see cref="Qty"/> restated in the item's base UOM, resolved once at line creation.
/// Confirm reads this snapshot rather than re-converting, so a conversion factor edited
/// between create and confirm cannot change what a saved GRN posts, and a later reversal
/// reproduces the original layer exactly.
/// </summary>
public decimal QtyBase { get; set; }
/// <summary>The factor used to derive <see cref="QtyBase"/>; 1 when the line is in base UOM.</summary>
public decimal ConversionFactor { get; set; } = 1m;
/// <summary>Gross unit cost received at (entered, or PO price when omitted).</summary>
public decimal UnitCost { get; set; }
+24
View File
@@ -21,8 +21,32 @@ public class PoLine
public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }
/// <summary>Quantity ordered, in <see cref="UomId"/>.</summary>
public decimal Qty { get; set; }
public decimal UnitPrice { get; set; }//
public decimal Tax { get; set; }
/// <summary>
/// <see cref="Qty"/> restated in the item's base UOM, resolved once at line creation.
/// This — not <see cref="Qty"/> — is what open-quantity and close checks compare against,
/// because a GRN may legitimately receive against this line in a different UOM.
/// </summary>
public decimal QtyBase { get; set; }
/// <summary>The factor used to derive <see cref="QtyBase"/>; 1 when the line is in base UOM.</summary>
public decimal ConversionFactor { get; set; } = 1m;
/// <summary>
/// Accrues as GRNs confirm (FR-PROC-07), in <see cref="UomId"/>. <b>Denormalized for
/// display only</b> — it is derived by dividing <see cref="QtyReceivedBase"/> by the
/// factor, so it can drift. Never branch on it; use <see cref="QtyReceivedBase"/>.
/// </summary>
public decimal QtyReceived { get; set; }
/// <summary>
/// Authoritative received-to-date in the item's base UOM. GRN confirm accrues here and
/// the PO close condition compares this against <see cref="QtyBase"/>, so receipts in a
/// UOM other than the PO's still add up correctly.
/// </summary>
public decimal QtyReceivedBase { get; set; }
}
@@ -14,10 +14,24 @@ public class SalesInvoiceLine
public string Description { get; set; } = string.Empty;
/// <summary>Quantity sold, in <see cref="UomId"/> — what the user entered and what prints.</summary>
public decimal Qty { get; set; }
public decimal FreeQty { get; set; }
public int UomId { get; set; }
public Uom? Uom { get; set; }
/// <summary>
/// <see cref="Qty"/> restated in the item's base UOM, resolved once at save. Posting
/// consumes stock against this snapshot — the FIFO engine is base-UOM only, so passing
/// the entered quantity would deplete the wrong amount whenever the line is not in base UOM.
/// </summary>
public decimal QtyBase { get; set; }
/// <summary><see cref="FreeQty"/> restated in the item's base UOM.</summary>
public decimal FreeQtyBase { get; set; }
/// <summary>The factor used to derive the base quantities; 1 when the line is in base UOM.</summary>
public decimal ConversionFactor { get; set; } = 1m;
public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }
@@ -14,10 +14,24 @@ public class SalesSlipLine
public string Description { get; set; } = string.Empty;
/// <summary>Quantity sold, in <see cref="UomId"/> — what the user entered and what prints.</summary>
public decimal Qty { get; set; }
public decimal FreeQty { get; set; }
public int UomId { get; set; }
public Uom? Uom { get; set; }
/// <summary>
/// <see cref="Qty"/> restated in the item's base UOM, resolved once at save. Posting
/// consumes stock against this snapshot — the FIFO engine is base-UOM only, so passing
/// the entered quantity would deplete the wrong amount whenever the line is not in base UOM.
/// </summary>
public decimal QtyBase { get; set; }
/// <summary><see cref="FreeQty"/> restated in the item's base UOM.</summary>
public decimal FreeQtyBase { get; set; }
/// <summary>The factor used to derive the base quantities; 1 when the line is in base UOM.</summary>
public decimal ConversionFactor { get; set; } = 1m;
public int WarehouseId { get; set; }
public Warehouse? Warehouse { get; set; }