瀏覽代碼

Updated Ordering screen to use new system. (DON'T PULL, BECAUSE CODE BROKEN)

Kenric Nugteren 9 月之前
父節點
當前提交
a5b31e3a97

+ 0 - 15
PRSDimensionUtils/PRSDimensionUtils.csproj

@@ -1,15 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <TargetFramework>net8.0-windows</TargetFramework>
-        <Nullable>enable</Nullable>
-        <UseWPF>true</UseWPF>
-        <ImplicitUsings>enable</ImplicitUsings>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <ProjectReference Include="..\..\inabox\inabox.scripting\InABox.Scripting.csproj" />
-      <ProjectReference Include="..\prs.classes\PRSClasses.csproj" />
-    </ItemGroup>
-
-</Project>

+ 1 - 16
prs.classes/Entities/PurchaseOrder/PurchaseOrderItem.cs

@@ -45,8 +45,7 @@ namespace Comal.Classes
         public string Allocations { get; set; }
         
         [EntityRelationship(DeleteAction.SetNull)]
-        [NullEditor]
-        [Obsolete("Replaced with Allocations Aggregate", true)]
+        [EditorSequence(4)]
         public JobLink Job { get; set; }
 
         [MemoEditor(Visible = Visible.Default)]
@@ -64,7 +63,6 @@ namespace Comal.Classes
         [CurrencyEditor(Visible = Visible.Default)]
         [EditorSequence(9)]
         public double Cost { get; set; }
-
         
         [CurrencyEditor(Visible = Visible.Default, Summary = Summary.Sum)]
         [EditorSequence(10)]
@@ -182,9 +180,6 @@ namespace Comal.Classes
         [ComplexFormula(typeof(OpenFormsAggregate))]
         public int OpenForms { get; set; }
         
-        [ChildEntity(typeof(PurchaseOrderItemAllocationNominatedJob))]
-        public PurchaseOrderItemAllocationJobLink NominatedJob { get; set; }
-        
         static PurchaseOrderItem()
         {
             LinkedProperties.Register<PurchaseOrderItem, ProductLink, String>(x => x.Product, x => x.Code, x => x.SupplierCode);
@@ -267,14 +262,4 @@ namespace Comal.Classes
             
         }
     }
-    
-    public class PurchaseOrderItemAllocationNominatedJob : IChildEntityDefinition<PurchaseOrderItemAllocation>
-    {
-        public Filter<PurchaseOrderItemAllocation>? Filter { get; set; } = new Filter<PurchaseOrderItemAllocation>(x => x.Nominated).IsEqualTo(true);
-
-        public SortOrder<PurchaseOrderItemAllocation>? Sort { get; set; }
-            = new SortOrder<PurchaseOrderItemAllocation>(x => x.Job.JobNumber, SortDirection.Descending).ThenBy(x => x.Created, SortDirection.Descending);
-
-        public Expression<Func<PurchaseOrderItemAllocation, Guid>> Parent => x => x.Item.ID;
-    }
 }

+ 28 - 6
prs.classes/Entities/PurchaseOrder/PurchaseOrderItemAllocation.cs

@@ -6,6 +6,34 @@ using System.Text;
 
 namespace Comal.Classes
 {
+    /// <summary>
+    /// The <see cref="PurchaseOrderItemAllocation"/> is the way that we are allowing a single <see cref="PurchaseOrderItem"/> to, when received,
+    /// have its stock distributed to multiple sources. The current idea is this:
+    /// 
+    /// <list type="bullet">
+    ///     <item>
+    ///         The <see cref="PurchaseOrderItem"/> itself has a <see cref="PurchaseOrderItem.Job"/> and a <see cref="PurchaseOrderItem.Qty"/>.
+    ///         In most cases, this will be the target for received stock, and
+    ///         in this simple case, there are no POIAs. If the <see cref="PurchaseOrderItem.Job"/> is blank, it goes to general stock;
+    ///         otherwise, it goes to a specific allocation.
+    ///     </item>
+    ///     <item>
+    ///         If there are allocations, this cuts into the <see cref="PurchaseOrderItem.Qty"/>, such that after any allocations have been filled,
+    ///         the remainder of the stock goes to the <see cref="PurchaseOrderItem.Job"/>.
+    ///         If the allocations total more than <see cref="PurchaseOrderItem.Qty"/>, then a negative stock movement
+    ///         is registered against <see cref="PurchaseOrderItem.Job"/>.
+    ///     </item>
+    ///     <item>
+    ///         In the case of Dimension conversions (see <see cref="DimensionUnit.Conversion"/>), the quantity on the allocation is after the conversion has
+    ///         happened.<br/>
+    ///         This is important, because in this case, <b>there will be a discrepancy</b> between <see cref="PurchaseOrderItem.Qty"/> and the total
+    ///         of <see cref="Quantity"/>.
+    ///         <br/>
+    ///         As an example, suppose we are purchasing 2 boxes of 1000 screws. Then, the allocations are by numbers of screws, so that 800 might go to
+    ///         Job A, 600 to Job B and 600 to general stock. So the allocation quantities are the actual numbers that will be on the stock movements.
+    ///     </item>
+    /// </list>
+    /// </summary>
     [Caption("Allocation")]
     public class PurchaseOrderItemAllocation : Entity, IRemotable, IPersistent, ILicense<ProjectManagementLicense>
     , IOneToMany<JobRequisitionItem>, IOneToMany<Job>, IOneToMany<PurchaseOrderItem>
@@ -13,9 +41,6 @@ namespace Comal.Classes
         [EntityRelationship(DeleteAction.Cascade)]
         public PurchaseOrderItemLink Item { get; set; }
         
-        /// <summary>
-        /// This may not be blank.
-        /// </summary>
         [EntityRelationship(DeleteAction.Cascade)]
         [EditorSequence(1)]
         public JobLink Job { get; set; }
@@ -44,9 +69,6 @@ namespace Comal.Classes
         [EditorSequence(3)]
         public double Quantity { get; set; }
 
-        [NullEditor]
-        public bool Nominated { get; set; }
-
         protected override void DoPropertyChanged(string name, object? before, object? after)
         {
             base.DoPropertyChanged(name, before, after);

+ 15 - 55
prs.desktop/Panels/Reservation Management/ReservationManagementItemGrid.cs

@@ -338,37 +338,23 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
             return false;
         }
 
-        var menu = new ContextMenu()
-        {
-            Items =
-            {
-                new MenuItem()
-                {
-                    Header = "Create Purchase Order",
-                    Command = new ActionCommand(CreatePurchaseOrder)
-                },
-                new Separator(),
-                new MenuItem()
-                {
-                    Header = "Add To Existing Order",
-                    Command = new ActionCommand(AddToExistingOrder)
-                }
-            }
-        };
+        var menu = new ContextMenu();
+        menu.AddItem("Create Purchase Order", null, rows, CreatePurchaseOrder);
+        menu.AddItem("Add to Existing Order", null, rows, AddToExistingOrder);
         menu.IsOpen = true;
 
         return false;
 
     }
 
-    private void CreatePurchaseOrder()
+    private void CreatePurchaseOrder(CoreRow[] rows)
     {
         var mr = MessageWindow.ShowYesNoCancel("Include Style in Order?", "Confirm");
         if (mr == MessageWindowResult.Cancel)
             return;
         
         var items = new List<StockForecastOrderData>();
-        foreach(var row in SelectedRows)
+        foreach(var row in rows)
         {
             var jri = row.ToObject<JobRequisitionItem>();
             if (mr != MessageWindowResult.Yes)
@@ -376,9 +362,7 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
                 jri.Style.CopyFrom(new ProductStyle());
             }
 
-            var qtyrequired = row.Get<JobRequisitionItem, double>(c => c.Qty) - row.Get<JobRequisitionItem, double>(c => c.InStock);
-            
-            DimensionUtils.ConvertDimensions(jri.Dimensions,ref qtyrequired, (f,c) => Client.Query(f,c));
+            var qtyRequired = DimensionUtils.ConvertDimensions(jri.Dimensions, jri.Qty - jri.InStock, (f,c) => Client.Query(f,c));
 
             var item = items.FirstOrDefault(x =>
                 x.Product.ID == jri.Product.ID
@@ -387,35 +371,12 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
             );
             if (item == null)
             {
-                item = new StockForecastOrderData(
-                    new ProductLink()
-                    {
-                        ID = jri.Product.ID,
-                        Code = jri.Product.Code,
-                        Name = jri.Product.Name
-                    },
-                    new ProductStyleLink()
-                    {
-                        ID = jri.Style.ID,
-                        Code = jri.Style.Code,
-                        Description = jri.Style.Description
-                    },
-                    new StockDimensions()
-                    {
-                        Unit = new ProductDimensionUnitLink() { ID = jri.Dimensions.Unit.ID },
-                        Length = jri.Dimensions.Length,
-                        Width = jri.Dimensions.Width,
-                        Height = jri.Dimensions.Height,
-                        Weight = jri.Dimensions.Weight,
-                        Quantity = jri.Dimensions.Quantity,
-                        Value = jri.Dimensions.Value,
-                        UnitSize = jri.Dimensions.UnitSize
-                    });
+                item = new StockForecastOrderData(jri.Product, jri.Style, jri.Dimensions);
                 items.Add(item);
             }
 
-            item.RequiredQuantity += qtyrequired;
-            item.SetRequiredQuantity(jri.Job.ID, jri.ID, $"{jri.Job.JobNumber}-{jri.Requisition.Number}",qtyrequired);
+            item.RequiredQuantity += qtyRequired;
+            item.SetRequiredQuantity(jri.Job.ID, jri.ID, $"{jri.Job.JobNumber}-{jri.Requisition.Number}", qtyRequired);
         }
 
         var window = new StockForecastOrderScreen(items, false) { Owner = Application.Current.MainWindow };
@@ -426,7 +387,7 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
         }
     }
 
-    private void AddToExistingOrder()
+    private void AddToExistingOrder(CoreRow[] rows)
     {
         var dlg = new MultiSelectDialog<PurchaseOrder>(
             new Filter<PurchaseOrder>(x => x.ClosedDate).IsEqualTo(DateTime.MinValue),
@@ -442,15 +403,14 @@ public class ReservationManagementItemGrid : DynamicDataGrid<JobRequisitionItem>
 
     #region CreatePickingList
 
-    private bool DoCreatePickingList(Button button, CoreRow[]? rows)
+    private bool DoCreatePickingList(Button button, CoreRow[] rows)
     {
-        if (rows?.Any() != true)
+        if (rows.Length == 0)
             return false;
-        if (rows.All(r =>
-                r.Get<JobRequisitionItem, double>(x => x.PickRequested)
-                    .IsEffectivelyEqual(r.Get<JobRequisitionItem, double>(x => x.Qty))))
+
+        if (rows.All(r => r.Get<JobRequisitionItem, double>(x => x.PickRequested).IsEffectivelyEqual(r.Get<JobRequisitionItem, double>(x => x.Qty))))
         {
-            MessageWindow.ShowMessage("All Items have been picked!","Error");
+            MessageWindow.ShowMessage("All Items have been picked!", "Error", image: MessageWindow.WarningImage);
             return false;
         }
         

+ 7 - 0
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderScreen.xaml.cs

@@ -162,9 +162,13 @@ public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
                     var orderItem = new PurchaseOrderItem();
                     orderItem.Product.ID = item.Item.Product.ID;
                     orderItem.Style.ID = item.Item.Style.ID;
+                    orderItem.Job.ID = item.SupplierProduct.Job.ID;
                     orderItems[item] = orderItem;
                 }
 
+                LookupFactory.DoLookups<PurchaseOrderItem, Job, JobLink>(
+                    orderItems.Values.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Job.ID)),
+                    x => x.Job);
                 LookupFactory.DoLookups<PurchaseOrderItem, Product, ProductLink>(
                     orderItems.Values.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Product.ID)),
                     x => x.Product);
@@ -204,6 +208,9 @@ public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
                 {
                     foreach (var breakup in result.Breakups)
                     {
+                        // Skip the nominated job.
+                        if (breakup.JobID == poi.Job.ID && breakup.JobRequiItemID == Guid.Empty) continue;
+
                         var alloc = new PurchaseOrderItemAllocation();
                         alloc.Item.ID = poi.ID;
                         alloc.Job.ID = breakup.JobID;

+ 99 - 97
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderingGrid.cs

@@ -8,7 +8,6 @@ using NPOI.SS.Formula.Functions;
 using Syncfusion.Data;
 using Syncfusion.Data.Extensions;
 using Syncfusion.UI.Xaml.Grid;
-using Syncfusion.Windows.Shared;
 using System;
 using System.Collections;
 using System.Collections.Generic;
@@ -24,6 +23,7 @@ using System.Windows.Media.Imaging;
 using Columns = InABox.Core.Columns;
 using PRSDesktop.Panels.StockForecast.OrderScreen;
 using PRSDimensionUtils;
+using Syncfusion.Windows.Shared;
 
 namespace PRSDesktop;
 
@@ -56,6 +56,7 @@ public class StockForecastOrderData(ProductLink product, ProductStyleLink style,
 
     public class QuantityBreakup(Guid jobID, Guid requiID, string description, double qty)
     {
+        // This may be blank, in which case this refers to General Stock.
         public Guid JobID { get; set; } = jobID;
 
         public Guid JobRequiItemID { get; set; } = requiID;
@@ -146,7 +147,10 @@ public class StockOrderingItem : BaseObject
     [EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
     public SupplierProductOrderStrategy OrderStrategy { get; set; }
 
-    private Dictionary<StockForecastBreakupKey, double> JobRequiredQuantities { get; set; } = [];
+    private Dictionary<StockForecastBreakupKey, double> JobRequiredQuantities { get; set; } = new()
+    {
+        { new(Guid.Empty, Guid.Empty), 0.0 }
+    };
     public Dictionary<StockForecastBreakupKey, double> GetJobRequiredQuantities()
     {
         return JobRequiredQuantities;
@@ -167,6 +171,10 @@ public class StockOrderingItem : BaseObject
         ? Quantities.Sum(x => x.OrderTotal)
         : Quantities.Sum(x => x.JobTotal);
 
+    public double GetRequiredQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
+        ? RequiredQuantity
+        : JobRequiredQuantities.Values.Sum();
+
     public void SetQuantities(StockForecastOrderingItemQuantity[] quantities)
     {
         Quantities = quantities;
@@ -203,13 +211,11 @@ public enum StockForecastOrderingStrategy
 
 public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>, ISpecificGrid
 {
+    #region Internal Data + Caches
+
     private List<SupplierProduct> SupplierProducts = [];
     private SupplierLink[] Suppliers = [];
 
-    public IList<StockForecastOrderData> OrderData { get; set; }
-
-    public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType));
-
     private DynamicActionColumn[] SupplierProductColumns = [];
     private DynamicActionColumn[] QuantityColumns = [];
     private DynamicActionColumn[] CostColumns = [];
@@ -217,8 +223,14 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
     private readonly Dictionary<Guid, Job> JobDetails = [];
     private readonly Dictionary<Guid, JobRequisitionItem> JobRequiDetails = [];
 
+    #endregion
+
     private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
 
+    #region Public Properties
+
+    public IList<StockForecastOrderData> OrderData { get; set; }
+
     private StockForecastOrderingType _orderType = StockForecastOrderingType.Breakup;
     public StockForecastOrderingType OrderType
     {
@@ -254,7 +266,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             {
                 foreach (var item in Items)
                 {
-                    item.OrderStrategy = ForecastOrderStrategyToProductOrderStrategy(value, item.Product.OrderStrategy);
+                    item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(value, item.Product.OrderStrategy);
                     item.CustomStrategy = false;
                 }
                 CalculateQuantities(false);
@@ -263,20 +275,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         }
     }
 
-    private static SupplierProductOrderStrategy ForecastOrderStrategyToProductOrderStrategy(StockForecastOrderingStrategy strategy, SupplierProductOrderStrategy defaultValue)
-    {
-        return strategy switch
-        {
-            StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact,
-            StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy
-                .LowestOverallPrice,
-            StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice,
-            StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock,
-            StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp,
-            StockForecastOrderingStrategy.PerProduct or _ => defaultValue
-        };
-    }
-
+    public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType));
     public IEnumerable<StockForecastOrderingResult> Results
     {
         get
@@ -314,11 +313,28 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         }
     }
 
+    #endregion
+
     public StockForecastOrderingGrid()
     {
         HiddenColumns.Add(x => x.Product.Image.ID);
     }
 
+    private static SupplierProductOrderStrategy CastOrderStrategyToProductOrderStrategy(StockForecastOrderingStrategy strategy, SupplierProductOrderStrategy defaultValue)
+    {
+        return strategy switch
+        {
+            StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact,
+            StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy
+                .LowestOverallPrice,
+            StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice,
+            StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock,
+            StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp,
+            StockForecastOrderingStrategy.PerProduct or _ => defaultValue
+        };
+    }
+
+
     #region UI Component
 
     private Component? _uiComponent;
@@ -399,11 +415,13 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
     private bool _loadedData = false;
     private void LoadData()
     {
-        var supplierColumns = Columns.None<SupplierProduct>().Add(x => x.ID)
+        var supplierProductColumns = Columns.None<SupplierProduct>().Add(x => x.ID)
             .Add(x => x.SupplierLink.ID)
             .Add(x => x.Product.ID)
             .Add(x => x.Style.ID)
             .Add(x => x.Style.Code)
+            .Add(x => x.Job.ID)
+            .Add(x => x.Job.JobNumber)
             .Add(x => x.ForeignCurrencyPrice)
             .Add(x => x.CostPrice)
             .AddDimensionsColumns(x => x.Dimensions)
@@ -412,7 +430,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         SupplierProducts = Client.Query(
             new Filter<SupplierProduct>(x => x.Product.ID).InList(OrderData.Select(x => x.Product.ID).ToArray())
                 .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
-            supplierColumns,
+            supplierProductColumns,
             new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
             .ToList<SupplierProduct>();
 
@@ -444,11 +462,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         var qty = item.GetQuantity(supplierIdx);
         qty.SupplierProduct = supplierProduct;
         qty.OrderTotal = 0;
-        qty.Breakups.Clear();
-        foreach(var id in item.GetJobRequiredQuantities().Keys)
-        {
-            qty.Breakups[id] = 0;
-        }
 
         return supplierProduct;
     }
@@ -473,19 +486,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             if(supplierIdx != -1)
             {
                 var qty = item.GetQuantity(supplierIdx);
-                if(OrderType == StockForecastOrderingType.StockOrder)
-                {
-                    qty.OrderTotal = GetRequiredQuantity(item, selectedSupplierProduct, item.RequiredQuantity);
-                }
-                else
-                {
-                    qty.Breakups.Clear();
-                    qty.OrderTotal = GetRequiredQuantity(item, selectedSupplierProduct, item.RequiredQuantity);
-                    foreach(var (id, q) in item.GetJobRequiredQuantities())
-                    {
-                        qty.Breakups[id] = q; // / item.RequiredQuantity * qty.OrderTotal;
-                    }
-                }
+                qty.OrderTotal = GetRequiredQuantity(item, selectedSupplierProduct);
             }
         }
     }
@@ -503,7 +504,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
                 item.Product.CopyFrom(dataItem.Product);
                 item.Style.CopyFrom(dataItem.Style);
                 item.Dimensions.CopyFrom(dataItem.Dimensions);
-                item.OrderStrategy = ForecastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
+                item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
                 item.RequiredQuantity = dataItem.RequiredQuantity;
                 foreach(var breakup in dataItem.GetRequiredQuantities())
                 {
@@ -534,43 +535,53 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
 
     #region Order Strategy
 
+    private double CalculateSupplierProductRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
+    {
+        var req = item.GetRequiredQuantity(OrderType);
+
+        var d = new StockDimensions();
+        d.CopyFrom(supplierProduct.Dimensions);
+        var result = DimensionUtils.ConvertDimensions(d, 1.0, (f,c) => Client.Query(f,c));
+        req = Math.Ceiling(req / result);
+        return req;
+    }
+
     private SupplierProduct? SelectSupplierProduct(IEnumerable<SupplierProduct> supplierProducts, StockOrderingItem item)
     {
+        double DimensionsRatio(SupplierProduct x)
+        {
+            return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value);
+        }
         switch (item.OrderStrategy)
         {
             case SupplierProductOrderStrategy.Exact:
             case SupplierProductOrderStrategy.RoundUp:
-                return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions) && x.Style.ID == item.Style.ID)
-                           .MinBy(x => x.CostPrice)
-                    ?? supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions))
-                        .MinBy(x => x.CostPrice);
+                return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions))
+                           .MinBy(x => x.CostPrice);
             
             case SupplierProductOrderStrategy.LowestOverallPrice:
-                return supplierProducts.Where(x => x.Style.ID == item.Style.ID)
-                           .MinBy(x => x.CostPrice *
-                                       Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value)))
-                       ?? supplierProducts
-                           .MinBy(x => x.CostPrice * Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value)));
-            
+                return supplierProducts.MinBy(x => x.CostPrice * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x)));
+
             case SupplierProductOrderStrategy.LowestUnitPrice:
-                return supplierProducts.Where(x => x.Style.ID == item.Style.ID)
-                           .MinBy(x=>x.CostPrice * item.Dimensions.Value/(x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value))
-                    ?? supplierProducts
-                        .MinBy(x=>x.CostPrice * item.Dimensions.Value/(x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value));
+                return supplierProducts.MinBy(x => x.CostPrice * DimensionsRatio(x));
             
             case SupplierProductOrderStrategy.LowestOverstock:
-                return supplierProducts.Where(x => x.Style.ID == item.Style.ID)
-                           .MinBy(x=>x.Dimensions.Value * Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value)))
-                       ?? supplierProducts
-                           .MinBy(x=>x.Dimensions.Value * Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value)));
+                return supplierProducts.MinBy(x => x.Dimensions.Value * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x)));
                 
             default:
                 return null;
         }
     }
 
-    private double GetRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct, double requiredQuantity)
+    private double GetRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
     {
+        var requiredQuantity = CalculateSupplierProductRequiredQuantity(item, supplierProduct);
+
+        double DimensionsRatio(SupplierProduct x)
+        {
+            return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value);
+        }
+
         var _strategy = item.CustomStrategy
             ? SupplierProductOrderStrategy.LowestOverstock
             : item.OrderStrategy;
@@ -584,7 +595,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             case SupplierProductOrderStrategy.LowestOverallPrice:
             case SupplierProductOrderStrategy.LowestUnitPrice:
             case SupplierProductOrderStrategy.LowestOverstock:
-                return Math.Ceiling(requiredQuantity * item.Dimensions.Value / (supplierProduct.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : supplierProduct.Dimensions.Value));
+                return Math.Ceiling(requiredQuantity * DimensionsRatio(supplierProduct));
             default:
                 return 0.0;
         }
@@ -666,9 +677,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
                 || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
                 || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName)
-                || new Column<SupplierProduct>(x => x.Job.ID).IsEqualTo(column.ColumnName)
-            //    || new Column<SupplierProduct>(x => x.Dimensions).IsEqualTo(column.ColumnName)
-            )
+                || new Column<SupplierProduct>(x => x.Job.ID).IsEqualTo(column.ColumnName))
             {
                 editor.Editable = editor.Editable.Combine(Editable.Disabled);
             }
@@ -766,6 +775,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         }
     }
 
+    #region Job Data Cache
+
     private void LoadJobData(IEnumerable<Guid> ids)
     {
         var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray();
@@ -799,6 +810,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         }
     }
 
+    #endregion
+
     private class QuantityControl : ContentControl
     {
         private readonly StockOrderingItem Item;
@@ -817,7 +830,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
         public void UpdateControl(StockForecastOrderingType mode)
         {
             // If no supplier product has been selected for this cell, we can't allow the user to select a quantity.
-            if(Item.GetQuantity(SupplierIndex).SupplierProduct is null)
+            var supplierProduct = Item.GetQuantity(SupplierIndex).SupplierProduct;
+            if(supplierProduct is null)
             {
                 Content = null;
                 return;
@@ -825,7 +839,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
 
             if(mode == StockForecastOrderingType.StockOrder)
             {
-                // Otherwise, simple quantity textbox editor.
                 var editor = new DoubleTextBox
                 {
                     VerticalAlignment = VerticalAlignment.Stretch,
@@ -878,15 +891,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
                 btn.Click += (o, e) =>
                 {
                     var qty = Item.GetQuantity(SupplierIndex);
-
-                    var alloc = qty.Breakups
-                        .Where(x => !Guid.Equals(x.Key.JobID,Guid.Empty) || !Guid.Equals(x.Key.RequiID,Guid.Empty))
-                        .Sum(x => x.Value);
-                    var order = qty.OrderTotal;
-                    var dim = new StockDimensions();
-                    dim.CopyFrom(qty.SupplierProduct.Dimensions);
-                    DimensionUtils.ConvertDimensions(dim, ref order, (f,c) => Client.Query(f,c));
-                    var general = order - alloc;
                     
                     Parent.LoadJobData(qty.Breakups.Keys.Select(x => x.JobID));
                     Parent.LoadJobRequiData(qty.Breakups.Keys.Select(x => x.RequiID));
@@ -919,20 +923,22 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
                     }).ToList();
 
                     var genitem = items.FirstOrDefault(x =>
-                        Guid.Equals(x.JobID, Guid.Empty) && Guid.Equals(x.JobRequiID, Guid.Empty));
+                        x.JobID == Guid.Empty && x.JobRequiID == Guid.Empty);
                     if (genitem == null)
                     {
                         genitem = new StockForecastOrderingJobItem
                         {
                             Description = "General Stock",
                             JobID = Guid.Empty,
-                            JobRequiID = Guid.Empty,
-
+                            JobRequiID = Guid.Empty
                         };
-                        items.Insert(0,genitem);
+                        items.Insert(0, genitem);
+                    }
+                    else
+                    {
+                        items.Remove(genitem);
+                        items.Insert(0, genitem);
                     }
-                    genitem.Quantity = general;
-
 
                     var window = new StockForecastOrderJobScreen();
                     window.Items = items;
@@ -943,12 +949,9 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
                             qty.Breakups[new(item.JobID, item.JobRequiID)] = item.Quantity;
                         }
                         qty.DoChanged();
-                        var req = items.Sum(x => x.Quantity);
-                        var x = 1.0;
-                        var d = new StockDimensions();
-                        d.CopyFrom(qty.SupplierProduct.Dimensions);
-                        DimensionUtils.ConvertDimensions(d, ref x, (f,c) => Client.Query(f,c));
-                        req = Math.Ceiling(req / x);
+
+                        var req = Parent.GetRequiredQuantity(Item, supplierProduct);
+
                         editor.Value = req;
                         qty.OrderTotal = req;
                     }
@@ -983,10 +986,13 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
 
             var comboBox = new ComboBox();
             comboBox.Tag = idx;
-            var items = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID)
-                .Select(x => new KeyValuePair<SupplierProduct?, string>(
-                    x,
-                    x.Style.ID != Guid.Empty ? $"{x.Dimensions.UnitSize}/{x.Style.Code}" : $"{x.Dimensions.UnitSize}"));
+
+            var supplierProducts = string.IsNullOrWhiteSpace(instance.Dimensions.Unit.Conversion)
+                ? SupplierProducts.Where(x => x.Dimensions.Equals(instance.Dimensions))
+                : SupplierProducts;
+
+            var items = supplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID)
+                .Select(x => new KeyValuePair<SupplierProduct?, string>(x, $"Job {x.Job.JobNumber}: {x.Dimensions.UnitSize}"));
             if (items.Any())
                 items = items.Prepend(new KeyValuePair<SupplierProduct?, string>(null, ""));
             comboBox.SelectedValuePath = "Key";
@@ -1003,10 +1009,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
                 instance.CustomStrategy = true;
                 var _item = LoadItem(row);
                 var _product = ((o as ComboBox)?.SelectedValue as SupplierProduct ?? new SupplierProduct());
-                qty.OrderTotal = GetRequiredQuantity(_item, _product, _item.RequiredQuantity);
-                qty.Breakups.Clear();
-                foreach(var (id, q) in _item.GetJobRequiredQuantities())
-                    qty.Breakups[id] = q;
+                qty.OrderTotal = GetRequiredQuantity(_item, _product);
                 
                 InvalidateRow(row);
             };
@@ -1015,7 +1018,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>
             comboBox.Margin = new Thickness(2);
             if(!items.Any())
                 comboBox.IsEnabled = false;
-            
 
             return comboBox;
         })

+ 22 - 15
prs.desktop/Panels/Stock Forecast/StockForecastGrid.cs

@@ -819,9 +819,7 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
                 {
                     var item = GetItem(new(instance.Product.ID, instance.Style.ID, instance.Dimensions));
 
-                    var minstock = (double)instance.MinimumStockLevel;
-                    DimensionUtils.ConvertDimensions(instance.Dimensions, ref minstock, (f,c) => Client.Query(f,c));
-                    item.MinStock += minstock;
+                    item.MinStock += DimensionUtils.ConvertDimensions(instance.Dimensions, instance.MinimumStockLevel, (f,c) => Client.Query(f,c));
                     item.IsProductInstance = true;
                 }
 
@@ -829,11 +827,9 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
                 foreach(var holdingrow in holdings.Rows)
                 {
                     var holding = holdingrow.ToObject<StockHolding>();
-                    var units = holding.Units;
-                    DimensionUtils.ConvertDimensions(holding.Dimensions, ref units, (f,c) => Client.Query(f,c));
-                    holding.Units = units;
+                    holding.Units = DimensionUtils.ConvertDimensions(holding.Dimensions, holding.Units, (f,c) => Client.Query(f,c));
+
                     var item = GetItem(GetKey(holding.Product.ID, holding.Style.ID, holding.Dimensions));
-                    //var jobID = holding.Get<StockHolding, Guid>(x => x.Job.ID);
                     if(holding.Job.ID == Guid.Empty)
                     {
                         item.GenStock += holding.Units;
@@ -849,9 +845,7 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
                     .ToDictionary(x => x.ID);
                 foreach (var poi in purchaseOrderItems.Values)
                 {
-                    var quantity = poi.Qty;
-                    DimensionUtils.ConvertDimensions(poi.Dimensions, ref quantity, (f,c) => Client.Query(f,c));
-                    poi.Qty = quantity;
+                    poi.Qty = DimensionUtils.ConvertDimensions(poi.Dimensions, poi.Qty, (f,c) => Client.Query(f,c));
                 }
                 
                 var allocations = results.Get<PurchaseOrderItemAllocation>();
@@ -861,7 +855,6 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
                     var q = allocation.Quantity;
                     DimensionUtils.ConvertDimensions(allocation.Item.Dimensions, ref q, (f,c) => Client.Query(f,c));
                     // POIAs are already converted where necessary, so we don't have to update the quantities again, just update the dimensions
-                    //allocation.Quantity = quantity;
                     
                     var key = new ItemKey(
                         allocation.Item.Product.ID,
@@ -869,12 +862,18 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
                         allocation.Item.Dimensions);
                     var item = GetItem(key);
 
-                    item.JobPO += allocation.Quantity;
-                    item.AddJobPO(allocation.Job.ID, allocation.Quantity);
+                    if(allocation.Job.ID == Guid.Empty)
+                    {
+                        item.GenPO += allocation.Quantity;
+                    }
+                    else
+                    {
+                        item.JobPO += allocation.Quantity;
+                        item.AddJobPO(allocation.Job.ID, allocation.Quantity);
+                    }
 
                     if(purchaseOrderItems.TryGetValue(allocation.Item.ID, out var poi))
                         poi.Qty -= allocation.Quantity;
-                    
                 }
 
                 foreach(var poi in purchaseOrderItems.Values)
@@ -885,7 +884,15 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
                         poi.Dimensions);
                     var item = GetItem(key);
 
-                    item.GenPO += poi.Qty;
+                    if(poi.Job.ID == Guid.Empty)
+                    {
+                        item.GenPO += poi.Qty;
+                    }
+                    else
+                    {
+                        item.JobPO += poi.Qty;
+                        item.AddJobPO(poi.Job.ID, poi.Qty);
+                    }
                 }
 
                 var jobBOMItems = results.Get<JobBillOfMaterialsItem>();

+ 0 - 1
prs.stores/PRSStores.csproj

@@ -24,7 +24,6 @@
     <ProjectReference Include="..\..\inabox\inabox.mailer.exchange\InABox.Mailer.Exchange.csproj" />
     <ProjectReference Include="..\..\inabox\inabox.wpf\InABox.Wpf.csproj" />
     <ProjectReference Include="..\prs.classes\PRSClasses.csproj" />
-    <ProjectReference Include="..\PRSDimensionUtils\PRSDimensionUtils.csproj" />
   </ItemGroup>
 
 </Project>

+ 6 - 1
prs.stores/Utilities/DimensionUtils.cs

@@ -51,5 +51,10 @@ public static class DimensionUtils
             quantity = Convert.ToDouble(script.GetValue("Quantity"));
         }
     }
-    
+
+    public static double ConvertDimensions(IDimensions dimensions, double quantity, Func<Filter<ProductDimensionUnit>?,Columns<ProductDimensionUnit>,CoreTable> reload)
+    {
+        ConvertDimensions(dimensions, ref quantity, reload);
+        return quantity;
+    }
 }