Browse Source

Added Order Strategies to Products and Stock Forecast Orders
Changes to Product UOM settings now propagate to all transactions
Product List / Add Supplier Product now correctly sets UOM and restricts blank suppliers

frogsoftware 9 months ago
parent
commit
903cd31757

+ 3 - 3
prs.classes/Entities/Product/Instance/ProductInstance.cs

@@ -24,14 +24,14 @@ namespace Comal.Classes
         public override AggregateCalculation Calculation => AggregateCalculation.Sum;
     }
 
-    public class ProductInstance : DimensionedEntity<StockDimensions>, IRemotable, IPersistent, IProductInstance,
+    public class ProductInstance : StockEntity, IRemotable, IPersistent, IProductInstance,
         ISequenceable, ILicense<ProductManagementLicense>
     {
 
         [NullEditor] public long Sequence { get; set; }
 
         [EditorSequence(1)] 
-        public ProductLink Product { get; set; }
+        public override ProductLink Product { get; set; }
 
         [EditorSequence(2)] 
         public ProductStyleLink Style { get; set; }
@@ -56,7 +56,7 @@ namespace Comal.Classes
         [LoggableProperty]
         public double NettCost { get; set; }
 
-        [CurrencyEditor(Visible = Visible.Optional)]
+        [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Disabled)]
         [EditorSequence(6)]
         [LoggableProperty]
         public double AverageCost { get; set; }

+ 214 - 0
prs.desktop/Panels/Products/Master List/ProductHoldingControl.cs

@@ -1,18 +1,66 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
 using Comal.Classes;
+using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
+using InABox.Wpf;
+using InABox.WPF;
 
 namespace PRSDesktop;
 
 public class ProductHoldingControl : DynamicDataGrid<StockHolding>, IProductControl
 {
+    
+    private Button AdjustValueButton;
+    
+    private Button RecalculateButton;
+    
     public ProductHoldingControl()
     {
         ColumnsTag = "ProductHolding";
+        HiddenColumns.Add(x => x.Product.ID);
+        HiddenColumns.Add(x => x.Job.ID);
+        HiddenColumns.Add(x => x.Job.JobNumber);
+        HiddenColumns.Add(x => x.Job.Name);
+        HiddenColumns.Add(x => x.Location.ID);
+        HiddenColumns.Add(x => x.Location.Code);
+        HiddenColumns.Add(x => x.Location.Description);
+        HiddenColumns.Add(x => x.Style.ID);
+        HiddenColumns.Add(x => x.Style.Code);
         HiddenColumns.Add(x => x.Qty);
+        HiddenColumns.Add(x => x.Units);
+        HiddenColumns.Add(x => x.Available);
+        HiddenColumns.Add(x => x.AverageValue);
+        HiddenColumns.Add(x => x.Dimensions.Unit.ID);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Description);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasHeight);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasLength);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasWidth);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasHeight);
+        HiddenColumns.Add(x => x.Dimensions.Unit.HasQuantity);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Format);
+        HiddenColumns.Add(x => x.Dimensions.Unit.Formula);
+        HiddenColumns.Add(x => x.Dimensions.Length);
+        HiddenColumns.Add(x => x.Dimensions.Width);
+        HiddenColumns.Add(x => x.Dimensions.Height);
+        HiddenColumns.Add(x => x.Dimensions.Quantity);
+        HiddenColumns.Add(x => x.Dimensions.Value);        
+        HiddenColumns.Add(x => x.Dimensions.UnitSize);
+        
+        AdjustValueButton = AddButton("Adjust Value", PRSDesktop.Resources.receipt.AsBitmapImage(), AdjustValues,
+            DynamicGridButtonPosition.Right);
+        AdjustValueButton.Margin = new Thickness(AdjustValueButton.Margin.Left, AdjustValueButton.Margin.Top, 10, AdjustValueButton.Margin.Bottom);
+        
+        RecalculateButton = AddButton("Recalculate", PRSDesktop.Resources.service.AsBitmapImage(), RecalculateHoldings,
+            DynamicGridButtonPosition.Right);
+        
     }
+    
     protected override void DoReconfigure(DynamicGridOptions options)
     {
         base.DoReconfigure(options);
@@ -49,4 +97,170 @@ public class ProductHoldingControl : DynamicDataGrid<StockHolding>, IProductCont
     //
     //     return result;
     // }
+    
+    protected override void SelectItems(CoreRow[]? rows)
+    {
+        base.SelectItems(rows);
+
+        var _groups = rows?.GroupBy(x => new Tuple<Guid, double>(
+            x.Get<StockHolding, Guid>(c => c.Product.ID),
+            x.Get<StockHolding, double>(c => c.Dimensions.Value))
+        );
+        AdjustValueButton.IsEnabled = (Product?.ID ?? Guid.Empty) != Guid.Empty && _groups?.Count() == 1;
+        
+        RecalculateButton.IsEnabled = (Product?.ID ?? Guid.Empty) != Guid.Empty;
+    }
+
+    
+    private bool AdjustValues(Button arg1, CoreRow[] rows)
+    {
+        if (rows?.Any() != true)
+            return false;
+        
+        double _newvalue = 0.0;
+        if (DoubleEdit.Execute("New Average Value", 0, double.MaxValue, ref _newvalue))
+        {
+            Progress.ShowModal("Creating Batch", progress =>
+            {
+                StockMovementBatch _batch = new StockMovementBatch()
+                {
+                    Type = StockMovementBatchType.Transfer,
+                    Employee = new EmployeeLink() { ID = App.EmployeeID },
+                    Notes = "Stock Value Adjustment"
+                };
+                Client.Save(_batch,"Stock value adjusted from Products List");
+                
+                progress.Report("Creating Movements");
+                List<StockMovement> _updates = new List<StockMovement>();
+                foreach (var _row in rows)
+                {
+                    var _holding = _row.ToObject<StockHolding>();
+                    _holding.AverageValue = _newvalue;
+                    _holding.Value = _newvalue * _holding.Units;
+                    Client.Save(_holding,"Stock value adjusted from Products List");
+                    _updates.AddRange(_holding.AdjustValue(_newvalue, _batch));
+                }
+                
+                progress.Report("Saving Movements");
+                Client.Save(_updates,"Stock value adjusted from Products List");
+            });
+            MessageBox.Show("All Done");
+            return true;
+        }
+
+        return false;
+    }
+    
+       private bool RecalculateHoldings(Button arg1, CoreRow[] arg2)
+    {
+        Dictionary<String, int> messages = new();
+        void AddMessage(String type)
+        {
+            messages.TryGetValue(type, out int count);
+            messages[type] = ++count;
+        }
+        
+        Progress.ShowModal("Recalculating", progress =>
+        {
+            progress.Report("Loading Data");
+            MultiQuery query = new MultiQuery();
+            
+            query.Add(
+                new Filter<StockHolding>(x => x.Product.ID).IsEqualTo(Product.ID),
+                Columns.Required<StockHolding>().Add(x => x.ID)
+                    .Add(x => x.Product.ID)
+                    .Add(x => x.Job.ID)
+                    .Add(x => x.Style.ID)
+                    .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
+                    .Add(x => x.Units)
+                    .Add(x => x.AverageValue)            
+                    .Add(x => x.Available)            
+                    .Add(x => x.Qty)
+                    .Add(x => x.Weight)
+                    .Add(x => x.Value)
+            );
+            
+            query.Add(
+                new Filter<StockMovement>(x => x.Product.ID).IsEqualTo(Product.ID),
+                Columns.None<StockMovement>().Add(x => x.ID)
+                    .Add(x => x.Product.ID)
+                    .Add(x => x.Job.ID)
+                    .Add(x => x.Style.ID)
+                    .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
+                    .Add(x => x.Units)
+                    .Add(x => x.Cost)
+                    .Add(x => x.JobRequisitionItem.ID)
+            );
+            query.Query();
+            var holdings = query.Get<StockHolding>().ToObjects<StockHolding>().ToList();
+            var toDelete = new List<StockHolding>();
+            var movements = query.Get<StockMovement>().ToObjects<StockMovement>().ToList();
+
+            progress.Report("Processing");
+            var updates = new List<StockHolding>();
+            
+            while (movements.Any())
+            {
+                var first = movements.First();
+                var selected = movements.Where(x => x.IsEqualTo(first)).ToList();
+
+                var holding = holdings.FirstOrDefault(x => x.IsEqualTo(first));
+                if (holding == null)
+                {
+                    holding = new StockHolding();
+                    holding.Location.ID = first.Location.ID;
+                    holding.Product.ID = Product.ID;
+                    holding.Style.ID = first.Style.ID;
+                    holding.Job.ID = first.Job.ID;
+                    holding.Dimensions.CopyFrom(first.Dimensions);
+                }
+                holding.Recalculate(selected);
+
+                // Removing from the list so that it is not deleted.
+                if (holdings.Contains(holding))
+                    holdings.Remove(holding);
+
+                if (holding.Units.IsEffectivelyEqual(0.0) && holding.Available.IsEffectivelyEqual(0.0))
+                {
+                    if(holding.ID != Guid.Empty)
+                    {
+                        toDelete.Add(holding);
+                    }
+                }
+                else if (holding.IsChanged())
+                {
+                    AddMessage(holding.ID != Guid.Empty ? "updated" : "added");
+                    updates.Add(holding);
+                }
+
+                movements.RemoveAll(x => selected.Any(s => s.ID == x.ID));
+            }
+
+            toDelete.AddRange(holdings);
+            foreach (var holding in toDelete)
+                AddMessage("deleted");
+
+            if (updates.Any())
+            {
+                progress.Report($"Updating {updates.Count} Holdings");
+                new Client<StockHolding>().Save(updates.Where(x => x.IsChanged()), "Updated by Recalculation");
+            }
+
+            if (toDelete.Any())
+            {
+                progress.Report($"Deleting {toDelete.Count} Holdings");
+                new Client<StockHolding>().Delete(toDelete, "Removed by Recalculation");
+            }
+
+        });
+        MessageWindow.ShowMessage(
+            messages.Any() 
+                ? String.Join("\n", messages.Select(x => $"{x.Value} holdings {x.Key}"))
+                : "Nothing to Update!"
+            ,"Recalculate");
+        return true;
+    }
+
+ 
+
 }

+ 70 - 0
prs.desktop/Panels/Products/Master List/ProductInstanceControl.cs

@@ -10,6 +10,8 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
 using System.Windows.Media.Imaging;
 
 namespace PRSDesktop;
@@ -18,6 +20,8 @@ public class ProductInstanceControl : DynamicDataGrid<ProductInstance>, IProduct
 {
     private readonly BitmapImage tick = PRSDesktop.Resources.tick.AsBitmapImage();
 
+    private Button AdjustValueButton;
+    
     public ProductInstanceControl()
     {
         ColumnsTag = "ProductInstance";
@@ -28,8 +32,21 @@ public class ProductInstanceControl : DynamicDataGrid<ProductInstance>, IProduct
         base.Init();
 
         HiddenColumns.Add(x => x.Product.DefaultInstance.ID);
+        
+        HiddenColumns.Add(x=>x.Product.ID);
+        HiddenColumns.Add(x=>x.Style.ID);
+        HiddenColumns.Add(x=>x.Dimensions.Unit.ID);
+        HiddenColumns.Add(x=>x.Dimensions.Height);
+        HiddenColumns.Add(x=>x.Dimensions.Width);
+        HiddenColumns.Add(x=>x.Dimensions.Length);
+        HiddenColumns.Add(x=>x.Dimensions.Quantity);
+        HiddenColumns.Add(x=>x.Dimensions.Weight);
 
         ActionColumns.Add(new DynamicImageColumn(IsDefaultImage, SelectDefaultAction) { Position = DynamicActionColumnPosition.End });
+        
+        AdjustValueButton = AddButton("Adjust Value", PRSDesktop.Resources.receipt.AsBitmapImage(), AdjustValues);
+        AdjustValueButton.Margin = new Thickness(10, AdjustValueButton.Margin.Top, AdjustValueButton.Margin.Right, AdjustValueButton.Margin.Bottom);
+
     }
 
     protected override void DoReconfigure(DynamicGridOptions options)
@@ -116,4 +133,57 @@ public class ProductInstanceControl : DynamicDataGrid<ProductInstance>, IProduct
         }
         base.Reload(criteria, columns, ref sort, token, action);
     }
+    
+    private bool AdjustValues(Button arg1, CoreRow[] rows)
+    {
+        if (rows?.Length != 1)
+            return false;
+        
+        double _newvalue = 0.0;
+        if (DoubleEdit.Execute("New Average Value", 0, double.MaxValue, ref _newvalue))
+        {
+            Progress.ShowModal("Updating Average Cost", progress =>
+            {
+                var _instance = rows[0].ToObject<ProductInstance>();
+                _instance.AverageCost = _newvalue;
+                Client.Save(_instance,"Updated from Product Master List");
+                
+                
+                progress.Report("Loading Holdings");
+                var _holdings = Client.Query<StockHolding>(
+                    new Filter<StockHolding>(x=>x.Product.ID).IsEqualTo(_instance.Product.ID)
+                        .And(x=>x.Style.ID).IsEqualTo(_instance.Style.ID)
+                        .And(x=>x.Dimensions).DimensionEquals(_instance.Dimensions)
+                        .And(x=>x.Job.ID).IsEqualTo(Guid.Empty),
+                    Columns.Required<StockHolding>()
+                    ).Rows.ToObjects<StockHolding>()
+                    .ToArray();
+                if (_holdings.Any())
+                {
+                    progress.Report("Creating Batch");
+                    StockMovementBatch _batch = new StockMovementBatch()
+                    {
+                        Type = StockMovementBatchType.Transfer,
+                        Employee = new EmployeeLink() { ID = App.EmployeeID },
+                        Notes = "Stock Value Adjustment"
+                    };
+                    Client.Save(_batch, "Stock value adjusted from Product Master List");
+
+
+                    progress.Report("Creating Movements");
+                    List<StockMovement> _updates = new List<StockMovement>();
+                    foreach (var _holding in _holdings)
+                        _updates.AddRange(_holding.AdjustValue(_newvalue, _batch));
+
+                    progress.Report("Saving Movements");
+                    Client.Save(_updates, "Stock value adjusted from Product Master List");
+                }
+            });
+            MessageBox.Show("All Done");
+            return true;
+        }
+
+        return false;
+    }
+    
 }

+ 18 - 10
prs.desktop/Panels/Products/Master List/ProductSuppliersControl.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Threading;
@@ -141,22 +142,22 @@ namespace PRSDesktop
 
         public override SupplierProduct CreateItem()
         {
-            var result = base.CreateItem();
-            result.Product.ID = Product.ID;
-            result.Product.Synchronise(Product);
+            var _result = base.CreateItem();
+            _result.Product.ID = Product.ID;
+            _result.Product.Synchronise(Product);
 
-            var ptbl = new Client<Product>().Query(
+            var _product = new Client<Product>().Query(
                 new Filter<Product>(c => c.ID).IsEqualTo(Product.ID),
-                Columns.None<Product>().AddDimensionsColumns(x => x.DefaultInstance.Dimensions));
-            var product = ptbl.Rows.FirstOrDefault()?.ToObject<Product>();
-            if (product != null)
-                result.Dimensions.CopyFrom(product.DefaultInstance.Dimensions);
-            return result;
+                Columns.None<Product>().AddSubColumns<ProductDimensionUnitLink>(x => x.UnitOfMeasure, null)
+            ).Rows.FirstOrDefault()?.ToObject<Product>() ?? new Product();
+            _result.Dimensions.Unit.CopyFrom(_product.UnitOfMeasure);
+            
+            return _result;
         }
 
         protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
         {
-            if (column.ColumnName.Equals("ProductLink.ID"))
+            if (column.ColumnName.Equals("Product.ID"))
                 return new NullEditor();
             return base.GetEditor(item, column);
         }
@@ -205,5 +206,12 @@ namespace PRSDesktop
             else
                 base.DoAdd();
         }
+
+        protected override void DoValidate(SupplierProduct[] items, List<string> errors)
+        {
+            base.DoValidate(items, errors);
+            if (items.Any(x=>x.SupplierLink.ID == Guid.Empty))
+                errors.Add("Supplier may not be blank!");
+        }
     }
 }

+ 23 - 8
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderScreen.xaml

@@ -5,7 +5,7 @@
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:local="clr-namespace:PRSDesktop.Panels.StockForecast.OrderScreen"
         mc:Ignorable="d"
-        Title="Order Stock" Height="800" Width="1200"
+        Title="Order Stock" Height="800" Width="1400"
         WindowStartupLocation="CenterScreen"
         x:Name="Window">
     <Grid DataContext="{Binding ElementName=Window}">
@@ -19,13 +19,28 @@
             <Label DockPanel.Dock="Left" Margin="5"
                    Content="Order Type: "
                    VerticalAlignment="Stretch" VerticalContentAlignment="Center"/>
-            <ComboBox x:Name="OrderTypeBox" DockPanel.Dock="Left" Margin="0,5,5,5" Width="90"
-                      VerticalContentAlignment="Center"
-                      SelectedValuePath="Tag" SelectedValue="{Binding OrderType}">
-                <ComboBoxItem Content="Job Order" Tag="{x:Static local:StockForecastOrderingType.JobOrder}"/>
-                <ComboBoxItem Content="Stock Order" Tag="{x:Static local:StockForecastOrderingType.StockOrder}"/>
-            </ComboBox>
-            <ComboBox x:Name="OrderStrategyBox" DockPanel.Dock="Left" Margin="0,5,5,5"/>
+            <ComboBox 
+                x:Name="OrderTypeBox" 
+                DockPanel.Dock="Left" 
+                Margin="0,5,5,5" 
+                Width="90"
+                VerticalContentAlignment="Center"
+                SelectionChanged="OrderTypeBox_OnSelectionChanged"/>
+            
+            <Label 
+                DockPanel.Dock="Left" 
+                Margin="5"
+                Content="Order Strategy: "
+                VerticalAlignment="Stretch" 
+                VerticalContentAlignment="Center"/>
+
+            <ComboBox 
+                x:Name="OrderStrategyBox" 
+                DockPanel.Dock="Left" 
+                Margin="0,5,5,5"
+                MinWidth="140"
+                SelectionChanged="OrderStrategyBox_OnSelectionChanged"/>
+            
             <Button x:Name="CancelButton" Click="CancelButton_Click"
                     Content="Cancel"
                     Margin="5" Padding="5" MinWidth="60"

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

@@ -17,14 +17,26 @@ using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Shapes;
+using InABox.Configuration;
 
 namespace PRSDesktop.Panels.StockForecast.OrderScreen;
 
+public class StockForecastOrderScreenSettings : IGlobalConfigurationSettings
+{
+    public StockForecastOrderingType OrderingType { get; set; } = 
+        StockForecastOrderingType.StockOrder;
+
+    public StockForecastOrderingStrategy OrderingStrategy { get; set; } =
+        StockForecastOrderingStrategy.LowestOverallPrice;
+}
+
 /// <summary>
 /// Interaction logic for StockForecastOrderScreen.xaml
 /// </summary>
 public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
 {
+    private StockForecastOrderScreenSettings settings;
+    
     private bool _canSave;
     public bool CanSave
     {
@@ -52,7 +64,20 @@ public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
 
     public StockForecastOrderScreen(List<StockForecastOrderData> items)
     {
+        settings = new GlobalConfiguration<StockForecastOrderScreenSettings>().Load();
+        
         InitializeComponent();
+        
+        OrderType = settings.OrderingType;
+        
+        OrderTypeBox.ItemsSource = Enum.GetValues<StockForecastOrderingType>()
+            .Select(x => new KeyValuePair<StockForecastOrderingType, string>(x, CoreUtils.Neatify(x.ToString())));
+        OrderTypeBox.SelectedValuePath = "Key";
+        OrderTypeBox.DisplayMemberPath = "Value";
+        OrderTypeBox.VerticalContentAlignment = VerticalAlignment.Center;
+        OrderTypeBox.Bind(ComboBox.SelectedValueProperty, this, x => x.OrderType);
+        
+        Strategy = settings.OrderingStrategy;
 
         OrderStrategyBox.ItemsSource = Enum.GetValues<StockForecastOrderingStrategy>()
             .Select(x => new KeyValuePair<StockForecastOrderingStrategy, string>(x, CoreUtils.Neatify(x.ToString())));
@@ -88,4 +113,20 @@ public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
     {
         CanSave = Grid.TotalQuantity > 0;
     }
+
+    private void OrderTypeBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (Grid.OrderData == null)
+            return;
+        settings.OrderingType = (StockForecastOrderingType)OrderTypeBox.SelectedValue;
+        new GlobalConfiguration<StockForecastOrderScreenSettings>().Save(settings);
+    }
+
+    private void OrderStrategyBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        if (Grid.OrderData == null)
+            return;
+        settings.OrderingStrategy = (StockForecastOrderingStrategy)OrderStrategyBox.SelectedValue;
+        new GlobalConfiguration<StockForecastOrderScreenSettings>().Save(settings);
+    }
 }

+ 59 - 25
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderingGrid.cs

@@ -184,11 +184,13 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
             if(_orderType != value)
             {
                 _orderType = value;
+                if (OrderData != null)
+                {
+                    CalculateQuantities(true);
+                    UIComponent.UpdateOrderType(OrderType);
 
-                CalculateQuantities(true);
-                UIComponent.UpdateOrderType(OrderType);
-
-                Refresh(true, true);
+                    Refresh(true, true);
+                }
             }
         }
     }
@@ -200,26 +202,33 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
         set
         {
             orderStrategy = value;
-
-            foreach(var item in Items)
+            if (OrderData != null)
             {
-                item.OrderStrategy = value switch
+                foreach (var item in Items)
                 {
-                    StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact,
-                    StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy.LowestOverallPrice,
-                    StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice,
-                    StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock,
-                    StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp,
-                    StockForecastOrderingStrategy.PerProduct or _ => item.Product.OrderStrategy
-                };
-                item.CustomStrategy = false;
+                    item.OrderStrategy = ForecastOrderStrategyToProductOrderStrategy(value, item.Product.OrderStrategy);
+                    item.CustomStrategy = false;
+                }
+                CalculateQuantities(false);
+                Refresh(false, true);
             }
-
-            CalculateQuantities(false);
-            Refresh(false, true);
         }
     }
 
+    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 IEnumerable<StockForecastOrderingResult> Results
     {
         get
@@ -421,6 +430,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
 
     private void CalculateQuantities(bool recreateItems)
     {
+        
         SetObserving(false);
 
         if (recreateItems)
@@ -435,7 +445,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                     item.Style.CopyFrom(dataItem.Style);
                     item.Dimensions.CopyFrom(dataItem.Dimensions);
                     item.RequiredQuantity = dataItem.RequiredQuantity;
-                    item.OrderStrategy = item.Product.OrderStrategy;
+                    item.OrderStrategy = ForecastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
                     Items.Add(item);
                 }
                 else
@@ -454,7 +464,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                         }
 
                         item.RequiredQuantity = q;
-                        item.OrderStrategy = item.Product.OrderStrategy;
+                        item.OrderStrategy = ForecastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
 
                         Items.Add(item);
                     }
@@ -487,10 +497,30 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
         {
             case SupplierProductOrderStrategy.Exact:
             case SupplierProductOrderStrategy.RoundUp:
-                // First, find the cheapest in the right style and dimensions.
-                return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions) && x.Style.ID == item.Style.ID).MinBy(x => x.CostPrice)
-                // Otherwise, find the cheapest in the right dimensions.
-                    ?? supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions)).MinBy(x => x.CostPrice);
+                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);
+            
+            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))
+                       ?? supplierProducts
+                           .MinBy(x => x.CostPrice * Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / x.Dimensions.Value));
+            
+            case SupplierProductOrderStrategy.LowestUnitPrice:
+                return supplierProducts.Where(x => x.Style.ID == item.Style.ID)
+                           .MinBy(x=>x.CostPrice * item.Dimensions.Value/x.Dimensions.Value)
+                    ?? supplierProducts
+                        .MinBy(x=>x.CostPrice * item.Dimensions.Value/x.Dimensions.Value);
+            
+            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))
+                       ?? supplierProducts
+                           .MinBy(x=>x.Dimensions.Value * Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / x.Dimensions.Value));
+                
             default:
                 return null;
         }
@@ -504,6 +534,10 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                 return item.RequiredQuantity;
             case SupplierProductOrderStrategy.RoundUp:
                 return Math.Ceiling(item.RequiredQuantity);
+            case SupplierProductOrderStrategy.LowestOverallPrice:
+            case SupplierProductOrderStrategy.LowestUnitPrice:
+            case SupplierProductOrderStrategy.LowestOverstock:
+                return Math.Ceiling(item.RequiredQuantity * item.Dimensions.Value / supplierProduct.Dimensions.Value);
             default:
                 return 0.0;
         }
@@ -559,7 +593,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
             })
             {
                 HeaderText = "Order Strategy.",
-                Width = 120
+                Width = 140
             });
 
             SupplierProductColumns = new DynamicActionColumn[Suppliers.Length];

+ 2 - 2
prs.desktop/Panels/Stock Forecast/StockForecastGrid.cs

@@ -238,11 +238,11 @@ public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataM
             if (Grid._supplierProducts == null)
                 return false;
 
-            var item = Grid.LoadItem(row);
+            var item = row.ToObject<StockForecastItem>(); //Grid.LoadItem(row));
             return Grid._supplierProducts.Any(r =>
                     Equals(r.Product.ID, item.Product.ID)
                     && Equals(r.Style.ID, item.Style.ID)
-                    && r.Dimensions.Equals(item.Dimensions)
+                    //&& r.Dimensions.Unit.ID.Equals(item.Dimensions.Unit.ID)
                     && Grid.SupplierIDs.Contains(r.SupplierLink.ID));
         }
         

+ 1 - 2
prs.desktop/Panels/Suppliers/SupplierProductGrid.cs

@@ -209,8 +209,7 @@ namespace PRSDesktop
         public override SupplierProduct CreateItem()
         {
             var result = base.CreateItem();
-            result.SupplierLink.ID = Supplier.ID;
-            result.SupplierLink.Synchronise(Supplier);
+            result.SupplierLink.CopyFrom(Supplier);
             return result;
         }
 

+ 1 - 1
prs.desktop/prsdesktop.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Desktop"
-#define MyAppVersion "8.16a"
+#define MyAppVersion "8.17"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSDesktop.exe"

+ 1 - 1
prs.licensing/PRSLicensing.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Licensing"
-#define MyAppVersion "8.16a"
+#define MyAppVersion "8.17"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSLicensing.exe"

+ 1 - 1
prs.server/PRSServer.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Server"
-#define MyAppVersion "8.16a"
+#define MyAppVersion "8.17"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSServer.exe"

+ 38 - 0
prs.stores/ProductStore.cs

@@ -11,6 +11,44 @@ namespace Comal.Stores
 
             if (entity.HasOriginalValue("NettCost"))
                 UpdateProductComponentCost(entity);
+            
+            if (entity.UnitOfMeasure.HasOriginalValue(nameof(ProductDimensionUnitLink.ID)))
+                UpdateProductUOMs(entity);
+        }
+
+        private static List<Type>? _stockentitytypes = null;
+        
+        private void UpdateProductUOMs(Product entity)
+        {
+            // If this is a new Product (ie original value is Guid.Empty)
+            if (entity.GetOriginalValue(x => x.ID, entity.ID) == Guid.Empty)
+                return;
+            
+            _stockentitytypes ??= CoreUtils.TypeList(x => x.IsSubclassOf(typeof(StockEntity)));
+            var _uom = Provider.Query(new Filter<ProductDimensionUnit>(x => x.ID).IsEqualTo(entity.UnitOfMeasure.ID))
+                .ToObjects<ProductDimensionUnit>().FirstOrDefault() ?? new ProductDimensionUnit();
+            //List<Task> _tasks = new List<Task>();
+            foreach (var _stockentitytype in _stockentitytypes)
+            {
+                //var _task = Task.Run(() =>
+                //{
+                    var _children = Provider.Query(
+                        _stockentitytype,
+                        new Filter<StockEntity>(x => x.Product.ID).IsEqualTo(entity.ID),
+                        Columns.None<StockEntity>()
+                            .Add(x => x.ID)
+                            .AddSubColumns(x => x.Dimensions, null)
+                    ).ToObjects(_stockentitytype).OfType<StockEntity>().ToArray();
+                    foreach (var _child in _children)
+                        _child.Dimensions.Unit.CopyFrom(_uom);
+                    var _updates = _children.Where(x => x.IsChanged()).ToArray();
+                    if (_updates.Any())
+                        Provider.Save(_stockentitytype,_updates);
+                    //});
+                    //_tasks.Add(_task);
+            }
+
+            //Task.WaitAll(_queries.ToArray());
         }
 
         protected override void AfterDelete(Product entity)