Explorar o código

Merge remote-tracking branch 'origin/kenric' into frank

frogsoftware hai 1 ano
pai
achega
af1ff7b53a

+ 106 - 0
prs.classes/EnclosedEntities/Dimensions/DimensionUnit.cs

@@ -1,3 +1,4 @@
+using InABox.Clients;
 using InABox.Core;
 using System;
 using System.Collections.Generic;
@@ -130,4 +131,109 @@ namespace Comal.Classes
             }
         }
     }
+
+    public static class DimensionUnitUtils
+    {
+        public static Dictionary<Type, int> UpdateExpressions<T, TLink>(T[] items)
+            where T : DimensionUnit, new()
+            where TLink : DimensionUnitLink<T>
+        {
+            var dimensionTypes = new List<(Type dimType, Type linkType)>();
+            foreach(var entity in CoreUtils.Entities)
+            {
+                var def = entity.GetSuperclassDefinition(typeof(Dimensions<,>));
+                if(def != null && def.GenericTypeArguments[1] == typeof(T))
+                {
+                    dimensionTypes.Add((entity, def.GenericTypeArguments[0]));
+                }
+            }
+
+            var updateTypes = new Dictionary<Type, List<IProperty>>();
+            foreach(var entity in CoreUtils.Entities)
+            {
+                if(entity.IsSubclassOf(typeof(Entity))
+                    && !entity.HasAttribute<AutoEntity>()
+                    && entity.HasInterface<IRemotable>()
+                    && entity.HasInterface<IPersistent>())
+                {
+                    if(entity == typeof(SupplierProduct))
+                    {
+                        var props = DatabaseSchema.Properties(entity).OrderBy(x => x.Name).ToArray();
+                    }
+                    foreach(var property in DatabaseSchema.Properties(entity))
+                    {
+                        if (property.Parent is null
+                            || property.Parent.Parent is null
+                            || property.IsCalculated
+                            || property.Parent.HasParentEntityLink()
+                            || !typeof(TLink).IsAssignableFrom(property.Parent.PropertyType)
+                            || !property.Name.EndsWith(".ID")) continue;
+
+                        var dimType = dimensionTypes.FirstOrDefault(x => property.Parent.Parent.PropertyType == x.dimType);
+                        if(dimType.dimType != null)
+                        {
+                            var dict = updateTypes.GetValueOrAdd(entity);
+                            dict.Add(property.Parent.Parent);
+                        }
+                    }
+                }
+            }
+
+            var nResults = new Dictionary<Type, int>();
+
+            var ids = items.ToArray(x => x.ID);
+            foreach(var (type, properties) in updateTypes)
+            {
+                var columns = Columns.Create(type, ColumnTypeFlags.Required);
+                foreach(var prop in properties)
+                {
+                    columns.Add(prop.Name + "." + Dimensions.unitid.Property);
+                    columns.Add(prop.Name + "." + Dimensions.quantity.Property);
+                    columns.Add(prop.Name + "." + Dimensions.length.Property);
+                    columns.Add(prop.Name + "." + Dimensions.width.Property);
+                    columns.Add(prop.Name + "." + Dimensions.height.Property);
+                    columns.Add(prop.Name + "." + Dimensions.weight.Property);
+                    columns.Add(prop.Name + "." + Dimensions.unitSize.Property);
+                    columns.Add(prop.Name + "." + Dimensions.value.Property);
+                }
+                IFilter? filter = null;
+                foreach(var prop in properties)
+                {
+                    var newFilter = Filter.Create(type, prop.Name + "." + Dimensions.unitid.Property, Operator.InList, ids);
+                    if(filter is null)
+                    {
+                        filter = newFilter;
+                    }
+                    else
+                    {
+                        filter = filter.Or(newFilter);
+                    }
+                }
+                if(filter != null)
+                {
+                    var results = Client.Create(type).Query(filter, columns).ToObjects(type).Cast<Entity>().ToArray();
+                    if(results.Length > 0)
+                    {
+                        foreach(var result in results)
+                        {
+                            foreach(var property in properties)
+                            {
+                                var id = CoreUtils.GetPropertyValue(result, property.Name + "." + Dimensions.unitid.Property);
+                                if(id is Guid guid)
+                                {
+                                    var unit = items.First(x => x.ID == guid);
+                                    var dim = (property.Getter()(result) as IDimensions)!;
+                                    dim.Calculate(dim.Quantity, dim.Length, dim.Width, dim.Height, dim.Weight, unit.Formula, unit.Format);
+                                }
+                            }
+                        }
+                        results = results.Where(x => x.IsChanged()).ToArray();
+                        nResults[type] = results.Length;
+                        Client.Create(type).Save(results, "Updated Value and UnitSize to match dimension unit.");
+                    }
+                }
+            }
+            return nResults;
+        }
+    }
 }

+ 51 - 56
prs.classes/EnclosedEntities/Dimensions/Dimensions.cs

@@ -106,21 +106,21 @@ namespace Comal.Classes
             {
 
                 if (unitid.IsEqualTo(name))
-                    Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
+                    (this as IDimensions).Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
                 else if (quantity.IsEqualTo(name))
-                    Calculate(after, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
+                    (this as IDimensions).Calculate(after, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
                 else if (length.IsEqualTo(name))
-                    Calculate(Quantity, after, Width, Height, Weight, Unit.Formula, Unit.Format);
+                    (this as IDimensions).Calculate(Quantity, after, Width, Height, Weight, Unit.Formula, Unit.Format);
                 else if (width.IsEqualTo(name))
-                    Calculate(Quantity, Length, after, Height, Weight, Unit.Formula, Unit.Format);
+                    (this as IDimensions).Calculate(Quantity, Length, after, Height, Weight, Unit.Formula, Unit.Format);
                 else if (height.IsEqualTo(name))
-                    Calculate(Quantity, Length, Width, after, Weight, Unit.Formula, Unit.Format);
+                    (this as IDimensions).Calculate(Quantity, Length, Width, after, Weight, Unit.Formula, Unit.Format);
                 else if (weight.IsEqualTo(name))
-                    Calculate(Quantity, Length, Width, Height, after, Unit.Formula, Unit.Format);
+                    (this as IDimensions).Calculate(Quantity, Length, Width, Height, after, Unit.Formula, Unit.Format);
                 else if (sizeformula.IsEqualTo(name))
-                    Calculate(Quantity, Length, Width, Height, Weight, after as string, Unit.Format);
+                    (this as IDimensions).Calculate(Quantity, Length, Width, Height, Weight, after as string, Unit.Format);
                 else if (sizeformat.IsEqualTo(name))
-                    Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, after as string);
+                    (this as IDimensions).Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, after as string);
             }
             finally
             {
@@ -148,7 +148,7 @@ namespace Comal.Classes
                 Width = width;
                 Height = height;
                 Weight = weight;
-                Calculate(quantity, length, width, height, weight, unit.Formula, unit.Format);
+                (this as IDimensions).Calculate(quantity, length, width, height, weight, unit.Formula, unit.Format);
             }
             finally
             {
@@ -158,47 +158,7 @@ namespace Comal.Classes
 
         public void CalculateValueAndUnitSize()
         {
-            Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
-        }
-
-        private void Calculate(object? quantity, object? length, object? width, object? height, object? weight, string? formula, string? format)
-        {
-            if (Evaluate<double>(formula, quantity, length, width, height, weight, out double value))
-                Value = value;
-            if (Evaluate<String>(format, quantity, length, width, height, weight, out string unitsize))
-                UnitSize = unitsize;
-        }
-
-        private bool Evaluate<T>(string? formula, object? quantity, object? length, object? width, object? height, object? weight, out T result)
-        {
-
-            if (!String.IsNullOrWhiteSpace(formula))
-            {
-                var variables = new Dictionary<string, object?>()
-                {
-                    { "Quantity", Convert.ToDouble(quantity) },
-                    { "Length", Convert.ToDouble(length) },
-                    { "Width", Convert.ToDouble(width) },
-                    { "Height", Convert.ToDouble(height) },
-                    { "Weight", Convert.ToDouble(weight) }
-                };
-                try
-                {
-                    var expr = new CoreExpression(formula);
-                    var eval = expr.Evaluate(variables);
-                    result = (T)CoreUtils.ChangeType(eval, typeof(T));
-                    return true;
-                }
-                catch (Exception e)
-                {
-                    Logger.Send(LogType.Information, "", String.Format("Error in Formula: [{0}] ({1})", formula, e.Message));
-                    result = default(T);
-                    return false;
-                }
-            }
-
-            result = default(T);
-            return true;
+            (this as IDimensions).Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
         }
 
         public void CopyFrom(IDimensions source, bool observing = false)
@@ -259,12 +219,15 @@ namespace Comal.Classes
     public static class Dimensions
     {
 
-        private static readonly Column<IDimensions> unitid = new Column<IDimensions>(x => x.Unit.ID);
-        private static readonly Column<IDimensions> quantity = new Column<IDimensions>(x => x.Quantity);
-        private static readonly Column<IDimensions> length = new Column<IDimensions>(x => x.Length);
-        private static readonly Column<IDimensions> width = new Column<IDimensions>(x => x.Width);
-        private static readonly Column<IDimensions> height = new Column<IDimensions>(x => x.Height);
-        private static readonly Column<IDimensions> weight = new Column<IDimensions>(x => x.Weight);
+        public static readonly Column<IDimensions> unitid = new Column<IDimensions>(x => x.Unit.ID);
+        public static readonly Column<IDimensions> quantity = new Column<IDimensions>(x => x.Quantity);
+        public static readonly Column<IDimensions> length = new Column<IDimensions>(x => x.Length);
+        public static readonly Column<IDimensions> width = new Column<IDimensions>(x => x.Width);
+        public static readonly Column<IDimensions> height = new Column<IDimensions>(x => x.Height);
+        public static readonly Column<IDimensions> weight = new Column<IDimensions>(x => x.Weight);
+
+        public static readonly Column<IDimensions> unitSize = new Column<IDimensions>(x => x.UnitSize);
+        public static readonly Column<IDimensions> value = new Column<IDimensions>(x => x.Value);
 
         public static IEnumerable<Column<IDimensions>> GetFilterColumns()
         {
@@ -457,5 +420,37 @@ namespace Comal.Classes
                 _ => LocalColumns<TDim>()
             });
         }
+
+        public static bool Evaluate<T>(string? formula, object? quantity, object? length, object? width, object? height, object? weight, out T result)
+        {
+
+            if (!String.IsNullOrWhiteSpace(formula))
+            {
+                var variables = new Dictionary<string, object?>()
+                {
+                    { "Quantity", Convert.ToDouble(quantity) },
+                    { "Length", Convert.ToDouble(length) },
+                    { "Width", Convert.ToDouble(width) },
+                    { "Height", Convert.ToDouble(height) },
+                    { "Weight", Convert.ToDouble(weight) }
+                };
+                try
+                {
+                    var expr = new CoreExpression(formula);
+                    var eval = expr.Evaluate(variables);
+                    result = (T)CoreUtils.ChangeType(eval, typeof(T));
+                    return true;
+                }
+                catch (Exception e)
+                {
+                    Logger.Send(LogType.Information, "", String.Format("Error in Formula: [{0}] ({1})", formula, e.Message));
+                    result = default(T);
+                    return false;
+                }
+            }
+
+            result = default(T);
+            return true;
+        }
     }
 }

+ 13 - 0
prs.classes/EnclosedEntities/Dimensions/IDimensions.cs

@@ -18,5 +18,18 @@ namespace Comal.Classes
 
         void Set(IDimensionUnit unit, double quantity, double length, double width, double height, double weight);
         void CopyFrom(IDimensions source, bool observing = false);
+
+        public void Calculate(object? quantity, object? length, object? width, object? height, object? weight, string? formula, string? format)
+        {
+            if (Dimensions.Evaluate<double>(formula, quantity, length, width, height, weight, out double value))
+                Value = value;
+            if (Dimensions.Evaluate<String>(format, quantity, length, width, height, weight, out string unitsize))
+                UnitSize = unitsize;
+        }
+
+        public void CalculateValueAndUnitSize()
+        {
+            this.Calculate(Quantity, Length, Width, Height, Weight, Unit.Formula, Unit.Format);
+        }
     }
 }

+ 11 - 0
prs.classes/Entities/Job/Requisitions/JobRequisitionItemPurchaseOrderItem.cs

@@ -6,6 +6,17 @@ using System.Text;
 
 namespace Comal.Classes
 {
+    public class PurchaseOrderItemAllocation : Entity, IRemotable, IPersistent, ILicense<ProjectManagementLicense>
+    {
+        public PurchaseOrderItemLink Item { get; set; }
+        
+        public JobLink Job { get; set; }
+        
+        public JobRequisitionItemLink JobRequisitionItem { get; set; }
+        
+        public double Quantity { get; set; }
+        
+    }
     
     public class JobRequisitionItemPurchaseOrderItem : Entity, IRemotable, IPersistent, IOneToMany<JobRequisitionItem>, ILicense<ProjectManagementLicense>
     {

+ 16 - 12
prs.classes/Entities/Product/Product.cs

@@ -75,6 +75,10 @@ namespace Comal.Classes
         [EditorSequence("Pricing", 1)]
         public ProductPricingStrategy PricingStrategy { get; set; } = ProductPricingStrategy.Standard;
 
+        [EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
+        [EditorSequence("Pricing", 2)]
+        public SupplierProductOrderStrategy OrderStrategy { get; set; }
+
         private class SupplierProductLookup : LookupDefinitionGenerator<SupplierProduct, Product>
         {
             public override Filter<SupplierProduct> DefineFilter(Product[] items)
@@ -87,53 +91,53 @@ namespace Comal.Classes
                 => Columns.None<Product>().Add(x => x.ID);
         }
         [LookupDefinition(typeof(SupplierProductLookup))]
-        [EditorSequence("Pricing", 2)]
+        [EditorSequence("Pricing", 3)]
         public ProductSupplierLink Supplier { get; set; }
 
         [CheckBoxEditor]
-        [EditorSequence("Pricing", 3)]
+        [EditorSequence("Pricing", 4)]
         public bool UseDefaultSupplierPricing { get; set; } = true;
 
         [CurrencyEditor(Visible = Visible.Optional)]
-        [EditorSequence("Pricing", 4)]
+        [EditorSequence("Pricing", 5)]
         public double BaseCost { get; set; }
 
         [Aggregate(typeof(ProductComponentCost))]
         [CurrencyEditor(Visible = Visible.Optional)] //, Editable = Editable.Disabled)]
-        [EditorSequence("Pricing", 5)]
+        [EditorSequence("Pricing", 6)]
         public double ComponentCost { get; set; }
 
         [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Disabled)]
-        [EditorSequence("Pricing", 6)]
+        [EditorSequence("Pricing", 7)]
         public double NettCost { get; set; }
         
         [Obsolete("Replaced with ProductInstance.AverageCost", true)]
         [CurrencyEditor(Visible = Visible.Optional)]
-        [EditorSequence("Pricing", 7)]
+        [EditorSequence("Pricing", 8)]
         [LoggableProperty]
         public double AverageCost { get; set; }
         
-        [EditorSequence("Pricing", 8)]
+        [EditorSequence("Pricing", 9)]
         [RequiredColumn]
         public TaxCodeLink TaxCode { get; set; }
 
-        [EditorSequence("Pricing", 9)]
+        [EditorSequence("Pricing", 10)]
         [RequiredColumn]
         public PurchaseGLCodeLink PurchaseGL { get; set; }
 
-        [EditorSequence("Pricing", 10)]
+        [EditorSequence("Pricing", 11)]
         [RequiredColumn]
         public SalesGLCodeLink SellGL { get; set; }
 
-        [EditorSequence("Pricing", 11)]
+        [EditorSequence("Pricing", 12)]
         [RequiredColumn]
         public CostCentreLink CostCentre { get; set; }
 
-        [EditorSequence("Pricing", 12)]
+        [EditorSequence("Pricing", 13)]
         public CostSheetSectionLink CostSheetSection { get; set; }
         
         [ProductChargeEditor]
-        [EditorSequence("Pricing", 13)]
+        [EditorSequence("Pricing", 14)]
         public ProductCharge Charge { get; set; }
         
         /// <summary>

+ 1 - 1
prs.classes/Entities/Product/ProductDimensions/ProductDimensionUnit.cs

@@ -3,7 +3,7 @@ using PRSClasses;
 
 namespace Comal.Classes
 {
-    public class ProductDimensionUnit : DimensionUnit, ILicense<ProductManagementLicense>
+    public class ProductDimensionUnit : DimensionUnit, ILicense<ProductManagementLicense>, IMergeable
     {
         
     }

+ 3 - 0
prs.classes/Entities/Product/ProductLink.cs

@@ -76,6 +76,9 @@ namespace Comal.Classes
 
         [EnumLookupEditor(typeof(ProductPricingStrategy), Visible = Visible.Optional, Editable = Editable.Hidden)]
         public ProductPricingStrategy PricingStrategy { get; set; }
+
+        [EnumLookupEditor(typeof(SupplierProductOrderStrategy), Visible = Visible.Optional, Editable = Editable.Hidden)]
+        public SupplierProductOrderStrategy OrderStrategy { get; set; }
         
         [NullEditor]
         [RequiredColumn]

+ 1 - 1
prs.classes/Entities/Stock/StockMovement/StockMovement.cs

@@ -155,7 +155,7 @@ namespace Comal.Classes
         [EditorSequence(13)]
         [EnumLookupEditor(typeof(StockMovementType), Visible = Visible.Default)]
         public StockMovementType Type { get; set; }
-        
+
         [EditorSequence(14)]
         [EntityRelationship(DeleteAction.SetNull)]
         public EmployeeLink Employee { get; set; }

+ 21 - 0
prs.classes/Entities/Supplier/SupplierProductOrderStrategy.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public enum SupplierProductOrderStrategy
+    {
+        /// <summary>
+        /// Find the supplier product that has exactly the right dimensions.
+        /// </summary>
+        Exact,
+        /// <summary>
+        /// Same as <see cref="SupplierProductOrderStrategy.Exact"/>, but round up the quantity.
+        /// </summary>
+        RoundUp,
+        LowestUnitPrice,
+        LowestOverallPrice,
+        LowestOverstock
+    }
+}

+ 92 - 22
prs.desktop/Grids/ProductDimensionUnitGrid.cs

@@ -1,39 +1,109 @@
+using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Windows;
 using Comal.Classes;
 using InABox.Core;
 using InABox.DynamicGrid;
+using InABox.Wpf;
+using InABox.WPF;
 
-namespace PRSDesktop.Grids
+namespace PRSDesktop.Grids;
+
+public class ProductDimensionUnitGrid : DynamicDataGrid<ProductDimensionUnit>
 {
-    public class ProductDimensionUnitGrid : DynamicDataGrid<ProductDimensionUnit>
+    protected override void Init()
+    {
+        base.Init();
+
+        AddButton("Update Expressions", null, (button, rows) =>
+        {
+            UpdateExpressions(rows.ToArray<ProductDimensionUnit>());
+            return false;
+        });
+    }
+
+    protected override void DoValidate(ProductDimensionUnit[] items, List<string> errors)
+    {
+        base.DoValidate(items, errors);
+        foreach (var item in items)
+            item.Validate(errors);
+    }
+
+    private void UpdateExpressions(ProductDimensionUnit[] items)
+    {
+        Dictionary<Type, int>? results = null;
+        Exception? exception = null;
+        Progress.ShowModal("Updating Dimensions", progress =>
+        {
+            try
+            {
+                results = DimensionUnitUtils.UpdateExpressions<ProductDimensionUnit, ProductDimensionUnitLink>(items);
+            }
+            catch(Exception e)
+            {
+                exception = e;
+            }
+        });
+        if(results is not null)
+        {
+            MessageWindow.ShowMessage($"Update successful:\n{string.Join("\n", results.Select(x => $"- {x.Key.Name}: {x.Value} items updated"))}", "Success");
+        }
+        else if(exception is not null)
+        {
+            MessageWindow.ShowError("Error while updating dimensions", exception);
+        }
+    }
+
+    private bool ShouldUpdateExpressions = false;
+
+    protected override void DoBeforeSave(IDynamicEditorForm editor, ProductDimensionUnit[] items)
     {
-        protected override void DoValidate(ProductDimensionUnit[] items, List<string> errors)
+        base.DoBeforeSave(editor, items);
+
+        ShouldUpdateExpressions = false;
+        if(items.Any(x => x.HasOriginalValue(x => x.Format) || x.HasOriginalValue(x => x.Formula)))
         {
-            base.DoValidate(items, errors);
-            foreach (var item in items)
-                item.Validate(errors);
+            if(MessageWindow.ShowYesNo("The format and/or formula has been changed; do you wish to update the UnitSize/Value of every item that uses dimension to match the new expression? (This may take a while)", "Update expressions?"))
+            {
+                ShouldUpdateExpressions = true;
+            }
         }
+    }
 
-        protected override void CustomiseEditor(ProductDimensionUnit[] items, DynamicGridColumn column, BaseEditor editor)
+    protected override void DoAfterSave(IDynamicEditorForm editor, ProductDimensionUnit[] items)
+    {
+        base.DoAfterSave(editor, items);
+
+        if (ShouldUpdateExpressions)
         {
-            base.CustomiseEditor(items, column, editor);
-            if (column.ColumnName == nameof(ProductDimensionUnit.Conversion) && editor is ScriptEditor scriptEditor)
+            UpdateExpressions(items);
+        }
+    }
+
+    protected override bool DoMerge(CoreRow[] rows)
+    {
+        return base.DoMerge(rows);
+    }
+
+    protected override void CustomiseEditor(ProductDimensionUnit[] items, DynamicGridColumn column, BaseEditor editor)
+    {
+        base.CustomiseEditor(items, column, editor);
+        if (column.ColumnName == nameof(ProductDimensionUnit.Conversion) && editor is ScriptEditor scriptEditor)
+        {
+            scriptEditor.Type = ScriptEditorType.TemplateEditor;
+            scriptEditor.OnEditorClicked += () =>
             {
-                scriptEditor.Type = ScriptEditorType.TemplateEditor;
-                scriptEditor.OnEditorClicked += () =>
+                var script = items.FirstOrDefault()?.Conversion.NotWhiteSpaceOr()
+                             ?? DimensionUnit.DefaultConvertDimensionsScript();
+
+                var editor = new ScriptEditorWindow(script, SyntaxLanguage.CSharp);
+                if (editor.ShowDialog() == true)
                 {
-                    var script = items.FirstOrDefault()?.Conversion.NotWhiteSpaceOr()
-                                 ?? DimensionUnit.DefaultConvertDimensionsScript();
-
-                    var editor = new ScriptEditorWindow(script, SyntaxLanguage.CSharp);
-                    if (editor.ShowDialog() == true)
-                    {
-                        foreach (var item in items)
-                            SetEditorValue(item, column.ColumnName, editor.Script);
-                    }
-                };
-            }
+                    foreach (var item in items)
+                        SetEditorValue(item, column.ColumnName, editor.Script);
+                }
+            };
         }
     }
 }

+ 14 - 0
prs.desktop/Panels/Products/Master List/ProductSuppliersControl.cs

@@ -44,6 +44,20 @@ namespace PRSDesktop
                 editor.CurrencySymbol = " "; // Non-empty, should blnak out "$" symbol
         }
 
+        public override DynamicGridColumns GenerateColumns()
+        {
+            var columns = new DynamicGridColumns();
+
+            columns.Add<SupplierProduct, string>(x => x.SupplierLink.Code, 120, "Supplier Code", "", Alignment.MiddleCenter);
+            columns.Add<SupplierProduct, string>(x => x.SupplierLink.Name, 0, "Supplier", "", Alignment.MiddleLeft);
+            columns.Add<SupplierProduct, string>(x => x.Dimensions.UnitSize, 120, "Size", "", Alignment.MiddleCenter);
+            columns.Add<SupplierProduct, string>(x => x.Style.Code, 120, "Style", "", Alignment.MiddleCenter);
+            columns.Add<SupplierProduct, string>(x => x.Job.JobNumber, 80, "Job", "", Alignment.MiddleCenter);
+            columns.Add<SupplierProduct, double>(x => x.CostPrice, 80, "Cost Price", "C2", Alignment.MiddleCenter);
+
+            return columns;
+        }
+
         public Product Product { get; set; }
 
         private bool SetDefaultClick(Button arg1, CoreRow[] arg2)

+ 0 - 26
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderJobScreen.xaml

@@ -1,26 +0,0 @@
-<Window x:Class="PRSDesktop.Panels.StockForecast.OrderScreen.StockForecastOrderJobScreen"
-        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-        xmlns:local="clr-namespace:PRSDesktop.Panels.StockForecast.OrderScreen"
-        mc:Ignorable="d"
-        Title="Select Job Quantities" Height="450" Width="600">
-    <Grid>
-        <Grid.RowDefinitions>
-            <RowDefinition Height="*"/>
-            <RowDefinition Height="Auto"/>
-        </Grid.RowDefinitions>
-        <local:StockForecastOrderingJobGrid x:Name="Grid" Margin="5,5,5,0"/>
-        <DockPanel Grid.Row="1" LastChildFill="False" x:Name="Buttons">
-            <Button x:Name="CancelButton" Click="CancelButton_Click"
-                    Content="Cancel"
-                    Margin="5" Padding="5" MinWidth="60"
-                    DockPanel.Dock="Right"/>
-            <Button x:Name="OKButton" Click="OKButton_Click"
-                    Content="OK"
-                    Margin="5,5,0,5" Padding="5" MinWidth="60"
-                    DockPanel.Dock="Right"/>
-        </DockPanel>
-    </Grid>
-</Window>

+ 0 - 50
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderJobScreen.xaml.cs

@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Shapes;
-
-namespace PRSDesktop.Panels.StockForecast.OrderScreen;
-
-/// <summary>
-/// Interaction logic for StockForecastOrderJobScreen.xaml
-/// </summary>
-public partial class StockForecastOrderJobScreen : Window
-{
-    public List<StockForecastOrderingJobItem> Items
-    {
-        get => Grid.Items;
-        set
-        {
-            Grid.Items = value;
-            Grid.Refresh(false, true);
-        }
-    }
-
-    public StockForecastOrderJobScreen()
-    {
-        InitializeComponent();
-
-        Grid.Refresh(true, false);
-    }
-
-    private void CancelButton_Click(object sender, RoutedEventArgs e)
-    {
-        DialogResult = false;
-        Close();
-    }
-
-    private void OKButton_Click(object sender, RoutedEventArgs e)
-    {
-        DialogResult = true;
-        Close();
-    }
-}

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

@@ -25,6 +25,7 @@
                 <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"/>
             <Button x:Name="CancelButton" Click="CancelButton_Click"
                     Content="Cancel"
                     Margin="5" Padding="5" MinWidth="60"

+ 18 - 2
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderScreen.xaml.cs

@@ -1,4 +1,7 @@
 using Comal.Classes;
+using InABox.Core;
+using InABox.Wpf;
+using InABox.WPF;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -39,13 +42,26 @@ public partial class StockForecastOrderScreen : Window, INotifyPropertyChanged
         set => Grid.OrderType = value;
     }
 
+    public StockForecastOrderingStrategy Strategy
+    {
+        get => Grid.OrderStrategy;
+        set => Grid.OrderStrategy = value;
+    }
+
     public IEnumerable<StockForecastOrderingResult> Results => Grid.Results;
 
-    public StockForecastOrderScreen(List<StockForecastOrderingItem> items)
+    public StockForecastOrderScreen(List<StockForecastOrderData> items)
     {
         InitializeComponent();
 
-        Grid.Items = items;
+        OrderStrategyBox.ItemsSource = Enum.GetValues<StockForecastOrderingStrategy>()
+            .Select(x => new KeyValuePair<StockForecastOrderingStrategy, string>(x, CoreUtils.Neatify(x.ToString())));
+        OrderStrategyBox.SelectedValuePath = "Key";
+        OrderStrategyBox.DisplayMemberPath = "Value";
+        OrderStrategyBox.VerticalContentAlignment = VerticalAlignment.Center;
+        OrderStrategyBox.Bind(ComboBox.SelectedValueProperty, this, x => x.Strategy);
+
+        Grid.OrderData = items;
         Grid.Refresh(true, true);
     }
 

+ 336 - 254
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderingGrid.cs

@@ -25,6 +25,28 @@ using Columns = InABox.Core.Columns;
 
 namespace PRSDesktop.Panels.StockForecast.OrderScreen;
 
+public class StockForecastOrderData(ProductLink product, ProductStyleLink style, StockDimensions dimensions)
+{
+    public ProductLink Product { get; set; } = product;
+
+    public ProductStyleLink Style { get; set; } = style;
+
+    public StockDimensions Dimensions { get; set; } = dimensions;
+
+    public double RequiredQuantity { get; set; }
+
+    private Dictionary<Guid, double> JobRequiredQuantities { get; set; } = [];
+
+    public Dictionary<Guid, double> GetJobRequiredQuantities()
+    {
+        return JobRequiredQuantities;
+    }
+    public void SetJobRequiredQuantity(Guid jobID, double requiredQty)
+    {
+        JobRequiredQuantities[jobID] = requiredQty;
+    }
+}
+
 public enum StockForecastOrderingType
 {
     StockOrder,
@@ -35,29 +57,35 @@ public class StockForecastOrderingItemQuantity
 {
     public event Action? Changed;
 
-    private double _stockTotal;
-    public double StockTotal
+    private double _total;
+    public double Total
     {
-        get => _stockTotal;
+        get => _total;
         set
         {
-            _stockTotal = value;
+            _total = value;
             Changed?.Invoke();
         }
     }
 
-    public Dictionary<Guid, double> JobTotals { get; init; } = [];
+    private SupplierProduct? _supplierProduct;
+    /// <summary>
+    /// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column.
+    /// </summary>
+    public SupplierProduct? SupplierProduct
+    {
+        get => _supplierProduct;
+        set
+        {
+            _supplierProduct = value;
+            Changed?.Invoke();
+        }
+    }
 
     public void DoChanged()
     {
         Changed?.Invoke();
     }
-
-    public double JobTotal => JobTotals.Sum(x => x.Value);
-
-    public double GetTotal(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
-        ? StockTotal
-        : JobTotal;
 }
 
 public class StockForecastOrderingItem : BaseObject
@@ -72,27 +100,25 @@ public class StockForecastOrderingItem : BaseObject
     public StockDimensions Dimensions { get; set; }
 
     [EditorSequence(4)]
+    public JobLink Job { get; set; }
+
+    [EditorSequence(5)]
     [DoubleEditor]
     public double RequiredQuantity { get; set; }
 
-    private Dictionary<Guid, double> JobRequiredQuantities { get; set; } = [];
+    [EditorSequence(6)]
+    [EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
+    public SupplierProductOrderStrategy OrderStrategy { get; set; }
 
-    public Dictionary<Guid, double> GetJobRequiredQuantities()
-    {
-        return JobRequiredQuantities;
-    }
-    public void SetJobRequiredQuantity(Guid jobID, double requiredQty)
-    {
-        JobRequiredQuantities[jobID] = requiredQty;
-    }
+    public bool CustomStrategy { get; set; } = false;
 
-    private StockForecastOrderingItemQuantity[] Quantities = [];
+    public StockForecastOrderingItemQuantity[] Quantities = [];
 
     public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i];
 
     public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
-        ? Quantities.Sum(x => x.StockTotal)
-        : Quantities.Sum(x => x.JobTotal);
+        ? Quantities.Sum(x => x.Total)
+        : Quantities.Sum(x => x.Total);
 
     public void SetQuantities(StockForecastOrderingItemQuantity[] quantities)
     {
@@ -122,13 +148,26 @@ public class StockForecastOrderingResult
     }
 }
 
+public enum StockForecastOrderingStrategy
+{
+    PerProduct,
+    Exact,
+    RoundUp,
+    LowestUnitPrice,
+    LowestOverallPrice,
+    LowestOverstock
+}
+
 public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrderingItem>, ISpecificGrid
 {
     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 = [];
 
@@ -146,13 +185,38 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
             {
                 _orderType = value;
 
-                CalculateQuantities();
+                CalculateQuantities(true);
+                UIComponent.UpdateOrderType(OrderType);
+
+                Refresh(true, true);
+            }
+        }
+    }
+
+    private StockForecastOrderingStrategy orderStrategy;
+    public StockForecastOrderingStrategy OrderStrategy
+    {
+        get => orderStrategy;
+        set
+        {
+            orderStrategy = value;
 
-                foreach(var control in QuantityControls)
+            foreach(var item in Items)
+            {
+                item.OrderStrategy = value switch
                 {
-                    control.UpdateControl(OrderType);
-                }
+                    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;
             }
+
+            CalculateQuantities(false);
+            Refresh(false, true);
         }
     }
 
@@ -166,25 +230,20 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                 foreach(var item in Items)
                 {
                     var qty = item.GetQuantity(i);
-                    var supplierProduct = GetSupplierProduct(item, supplier.ID);
-                    if (supplierProduct is null)
+                    if (qty.SupplierProduct is null)
                     {
-                        // If this is true, then the quantities also will have to be true.
                         continue;
                     }
 
-                    if(OrderType == StockForecastOrderingType.StockOrder && qty.StockTotal > 0)
+                    if(qty.Total > 0)
                     {
-                        yield return new(supplier, null, item, qty.StockTotal, supplierProduct);
-                    }
-                    else
-                    {
-                        foreach(var (jobID, q) in qty.JobTotals)
+                        if(OrderType == StockForecastOrderingType.StockOrder && qty.Total > 0)
+                        {
+                            yield return new(supplier, null, item, qty.Total, qty.SupplierProduct);
+                        }
+                        else
                         {
-                            if(q > 0)
-                            {
-                                yield return new(supplier, new() { ID = jobID }, item, q, supplierProduct);
-                            }
+                            yield return new(supplier, item.Job, item, qty.Total, qty.SupplierProduct);
                         }
                     }
                 }
@@ -221,7 +280,12 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
             Parent = grid;
             Grid = grid;
 
-            DataGrid.FrozenColumnCount = 7;
+            UpdateOrderType(grid.OrderType);
+        }
+
+        public void UpdateOrderType(StockForecastOrderingType type)
+        {
+            DataGrid.FrozenColumnCount = type == StockForecastOrderingType.StockOrder ? 8 : 9;
         }
 
         protected override Brush? GetCellSelectionBackgroundBrush()
@@ -238,7 +302,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                 var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
                 if(idx != -1)
                 {
-                    var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[idx].ID);
+                    var supplierProduct = item.GetQuantity(idx).SupplierProduct;
                     if(supplierProduct is null)
                     {
                         return new SolidColorBrush(Colors.Gainsboro);
@@ -285,13 +349,14 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
             .Add(x => x.SupplierLink.ID)
             .Add(x => x.Product.ID)
             .Add(x => x.Style.ID)
+            .Add(x => x.Style.Code)
             .Add(x => x.ForeignCurrencyPrice)
             .Add(x => x.CostPrice)
             .AddDimensionsColumns(x => x.Dimensions)
             .Add(x => x.SupplierLink.Code);
 
         SupplierProducts = Client.Query(
-            new Filter<SupplierProduct>(x => x.Product.ID).InList(Items.Select(x => x.Product.ID).ToArray())
+            new Filter<SupplierProduct>(x => x.Product.ID).InList(OrderData.Select(x => x.Product.ID).ToArray())
                 .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
             supplierColumns,
             new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
@@ -299,18 +364,9 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
 
         Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
 
-        foreach(var (itemIdx, item) in Items.WithIndex())
-        {
-            var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
-            for(int i = 0; i < Suppliers.Length; ++i)
-            {
-                quantities[i] = CreateQuantity(itemIdx);
-            }
-
-            item.SetQuantities(quantities);
-        }
+        LoadJobData(OrderData.SelectMany(x => x.GetJobRequiredQuantities().Keys).Distinct().Where(x => x != Guid.Empty));
 
-        CalculateQuantities();
+        CalculateQuantities(true);
 
         _loadedData = true;
     }
@@ -320,6 +376,8 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
         var qty = new StockForecastOrderingItemQuantity();
         qty.Changed += () =>
         {
+            if (!_observing) return;
+
             var row = Data.Rows[itemIdx];
             InvalidateRow(row);
             DoChanged();
@@ -327,82 +385,197 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
         return qty;
     }
 
-    private void CalculateQuantities()
+    private SupplierProduct CalculateSupplierProduct(StockForecastOrderingItem item, int supplierIdx)
     {
-        SetObserving(false);
-        foreach(var item in Items)
+        var supplierProduct = SelectSupplierProduct(SupplierProducts.Where(x => x.Product.ID == item.Product.ID && x.Style.ID == item.Style.ID && x.SupplierLink.ID == Suppliers[supplierIdx].ID), item);
+
+        var qty = item.GetQuantity(supplierIdx);
+        qty.SupplierProduct = supplierProduct;
+        qty.Total = 0;
+        return supplierProduct;
+    }
+
+    private void CalculateSupplierProduct(StockForecastOrderingItem item)
+    {
+        var selectedSupplierProducts = new List<SupplierProduct>();
+        for(int i = 0; i < Suppliers.Length; ++i)
         {
-            var supplierProduct = GetSupplierProduct(item);
-            for(int i = 0; i < Suppliers.Length; ++i)
+            var supplierProduct = CalculateSupplierProduct(item, i);
+            if(supplierProduct is not null)
             {
-                var qty = item.GetQuantity(i);
+                selectedSupplierProducts.Add(supplierProduct);
+            }
+        }
 
-                var supplier = Suppliers[i];
-                if(supplierProduct is not null && supplier.ID == supplierProduct.SupplierLink.ID)
+        var selectedSupplierProduct = SelectSupplierProduct(selectedSupplierProducts, item);
+        if(selectedSupplierProduct is not null)
+        {
+            var supplierIdx = Suppliers.WithIndex()
+                .FirstOrDefault(x => x.Value.ID == selectedSupplierProduct.SupplierLink.ID, new KeyValuePair<int, SupplierLink>(-1, null)).Key;
+            if(supplierIdx != -1)
+            {
+                item.GetQuantity(supplierIdx).Total = GetRequiredQuantity(item, selectedSupplierProduct);
+            }
+        }
+    }
+
+    private void CalculateQuantities(bool recreateItems)
+    {
+        SetObserving(false);
+
+        if (recreateItems)
+        {
+            Items.Clear();
+            foreach(var dataItem in OrderData)
+            {
+                if(OrderType == StockForecastOrderingType.StockOrder)
                 {
-                    if(OrderType == StockForecastOrderingType.StockOrder)
-                    {
-                        qty.StockTotal = qty.JobTotal;
-                    }
-                    else
-                    {
-                        qty.JobTotals.Clear();
-                        foreach(var (id, q) in item.GetJobRequiredQuantities())
-                        {
-                            qty.JobTotals[id] = q;
-                        }
-                    }
+                    var item = new StockForecastOrderingItem();
+                    item.Product.CopyFrom(dataItem.Product);
+                    item.Style.CopyFrom(dataItem.Style);
+                    item.Dimensions.CopyFrom(dataItem.Dimensions);
+                    item.RequiredQuantity = dataItem.RequiredQuantity;
+                    item.OrderStrategy = item.Product.OrderStrategy;
+                    Items.Add(item);
                 }
                 else
                 {
-                    if(OrderType == StockForecastOrderingType.StockOrder)
-                    {
-                        qty.StockTotal = 0;
-                    }
-                    else
+                    foreach(var (id, q) in dataItem.GetJobRequiredQuantities())
                     {
-                        foreach(var id in item.GetJobRequiredQuantities().Keys)
+                        var item = new StockForecastOrderingItem();
+                        item.Product.CopyFrom(dataItem.Product);
+                        item.Style.CopyFrom(dataItem.Style);
+                        item.Dimensions.CopyFrom(dataItem.Dimensions);
+                        item.Job.ID = id;
+
+                        if(id != Guid.Empty)
                         {
-                            qty.JobTotals[id] = 0;
+                            item.Job.CopyFrom(JobDetails[id]);
                         }
+
+                        item.RequiredQuantity = q;
+                        item.OrderStrategy = item.Product.OrderStrategy;
+
+                        Items.Add(item);
                     }
                 }
             }
         }
+
+        foreach(var (itemIdx, item) in Items.WithIndex())
+        {
+            var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
+            for(int i = 0; i < Suppliers.Length; ++i)
+            {
+                quantities[i] = CreateQuantity(itemIdx);
+            }
+
+            item.SetQuantities(quantities);
+        }
+
+        foreach(var item in Items)
+        {
+            CalculateSupplierProduct(item);
+        }
         SetObserving(true);
         DoChanged();
+    }
 
-        InvalidateGrid();
+    private SupplierProduct? SelectSupplierProduct(IEnumerable<SupplierProduct> supplierProducts, StockForecastOrderingItem item)
+    {
+        switch (item.OrderStrategy)
+        {
+            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);
+            default:
+                return null;
+        }
     }
 
+    private double GetRequiredQuantity(StockForecastOrderingItem item, SupplierProduct supplierProduct)
+    {
+        switch (item.OrderStrategy)
+        {
+            case SupplierProductOrderStrategy.Exact:
+                return item.RequiredQuantity;
+            case SupplierProductOrderStrategy.RoundUp:
+                return Math.Ceiling(item.RequiredQuantity);
+            default:
+                return 0.0;
+        }
+    }
+
+    private bool _loadedColumns = false;
     protected override DynamicGridColumns LoadColumns()
     {
         if (!_loadedData)
         {
             LoadData();
         }
-        ActionColumns.Clear();
-
-        ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
-        ActionColumns.Add(new DynamicImagePreviewColumn<StockForecastOrderingItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
 
         var columns = new DynamicGridColumns();
         columns.Add<StockForecastOrderingItem, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
         columns.Add<StockForecastOrderingItem, string>(x => x.Product.Name, 200, "Product Name", "", Alignment.MiddleLeft);
-        columns.Add<StockForecastOrderingItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter);
         columns.Add<StockForecastOrderingItem, string>(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter);
+        columns.Add<StockForecastOrderingItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter);
+        if(OrderType == StockForecastOrderingType.JobOrder)
+        {
+            columns.Add<StockForecastOrderingItem, string>(x => x.Job.JobNumber, 80, "Job No.", "", Alignment.MiddleCenter);
+        }
         columns.Add<StockForecastOrderingItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
 
-        QuantityColumns = new DynamicActionColumn[Suppliers.Length];
-        CostColumns = new DynamicActionColumn[Suppliers.Length];
-        QuantityControls.Clear();
-
-        for(int i = 0; i < Suppliers.Length; ++i)
+        if (!_loadedColumns)
         {
-            InitialiseSupplierColumn(i);
-        }
+            ActionColumns.Clear();
 
-        ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
+            ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
+            ActionColumns.Add(new DynamicImagePreviewColumn<StockForecastOrderingItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
+
+            ActionColumns.Add(new DynamicTemplateColumn(row =>
+            {
+                var item = LoadItem(row);
+
+                var box = new ComboBox();
+                box.ItemsSource = Enum.GetValues<SupplierProductOrderStrategy>()
+                    .Select(x => new KeyValuePair<SupplierProductOrderStrategy, string>(x, CoreUtils.Neatify(x.ToString())));
+                box.DisplayMemberPath = "Value";
+                box.SelectedValuePath = "Key";
+                box.SelectedValue = item.CustomStrategy ? null : item.OrderStrategy;
+                box.SelectionChanged += (o, e) =>
+                {
+                    if (box.SelectedValue is not SupplierProductOrderStrategy strategy) return;
+                    item.OrderStrategy = strategy;
+                    item.CustomStrategy = false;
+                    CalculateSupplierProduct(item);
+                    InvalidateRow(row);
+                };
+                box.Margin = new Thickness(2);
+                box.VerticalContentAlignment = VerticalAlignment.Center;
+                return box;
+            })
+            {
+                HeaderText = "Order Strategy.",
+                Width = 120
+            });
+
+            SupplierProductColumns = new DynamicActionColumn[Suppliers.Length];
+            QuantityColumns = new DynamicActionColumn[Suppliers.Length];
+            CostColumns = new DynamicActionColumn[Suppliers.Length];
+            QuantityControls.Clear();
+
+            for(int i = 0; i < Suppliers.Length; ++i)
+            {
+                InitialiseSupplierColumn(i);
+            }
+
+            ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
+
+            _loadedColumns = true;
+        }
 
         return columns;
     }
@@ -461,8 +634,6 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
 
                 foreach (var (itemIdx, item) in Items.WithIndex())
                 {
-                    var populateSupplierProduct = GetSupplierProduct(item);
-
                     var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length];
                     for (int i = 0; i < Suppliers.Length; ++i)
                     {
@@ -471,17 +642,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                     var newQty = CreateQuantity(itemIdx);
 
                     quantities[newIdx] = newQty;
-                    if (OrderType == StockForecastOrderingType.StockOrder)
-                    {
-                        newQty.StockTotal = 0;
-                    }
-                    else
-                    {
-                        foreach (var id in item.GetJobRequiredQuantities().Keys)
-                        {
-                            newQty.JobTotals[id] = 0;
-                        }
-                    }
+                    newQty.Total = 0;
                     item.SetQuantities(quantities);
                 }
 
@@ -511,7 +672,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
     {
         for(int idx = 0; idx < Suppliers.Length; ++idx)
         {
-            GetColumnGrouping().AddGroup(Suppliers[idx].Code, QuantityColumns[idx], CostColumns[idx]);
+            GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]);
         }
     }
 
@@ -549,102 +710,28 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
 
         public void UpdateControl(StockForecastOrderingType mode)
         {
-            var supplierProduct = Parent.GetSupplierProduct(Item, Parent.Suppliers[SupplierIndex].ID);
-            if(supplierProduct is null)
+            // 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)
             {
                 Content = null;
                 return;
             }
 
-            if(mode == StockForecastOrderingType.StockOrder)
+            // Otherwise, simple quantity textbox editor.
+            var editor = new DoubleTextBox
             {
-                var editor = new DoubleTextBox
-                {
-                    VerticalAlignment = VerticalAlignment.Stretch,
-                    HorizontalAlignment = HorizontalAlignment.Stretch,
-                    Background = new SolidColorBrush(Colors.LightYellow),
-                    BorderThickness = new Thickness(0.0),
-                    MinValue = 0.0,
-                    Value = Item.GetQuantity(SupplierIndex).StockTotal
-                };
-                editor.ValueChanged += (o, e) =>
-                {
-                    Item.GetQuantity(SupplierIndex).StockTotal = editor.Value ?? default;
-                };
-                Content = editor;
-            }
-            else if(mode == StockForecastOrderingType.JobOrder)
+                VerticalAlignment = VerticalAlignment.Stretch,
+                HorizontalAlignment = HorizontalAlignment.Stretch,
+                Background = new SolidColorBrush(Colors.LightYellow),
+                BorderThickness = new Thickness(0.0),
+                MinValue = 0.0,
+                Value = Item.GetQuantity(SupplierIndex).Total
+            };
+            editor.ValueChanged += (o, e) =>
             {
-                var grid = new Grid();
-                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-                grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
-
-                var editor = new TextBox
-                {
-                    VerticalAlignment = VerticalAlignment.Stretch,
-                    HorizontalAlignment = HorizontalAlignment.Stretch,
-                    VerticalContentAlignment = VerticalAlignment.Center,
-                    HorizontalContentAlignment = HorizontalAlignment.Center,
-                    Background = new SolidColorBrush(Colors.White),
-                    BorderThickness = new Thickness(0.0),
-                    IsReadOnly = true,
-                    Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal)
-                };
-                Grid.SetColumn(editor, 0);
-                grid.Children.Add(editor);
-
-                var btn = new Button
-                {
-                    VerticalAlignment = VerticalAlignment.Stretch,
-                    VerticalContentAlignment = VerticalAlignment.Center,
-                    HorizontalAlignment = HorizontalAlignment.Stretch,
-                    Content = "..",
-                    Margin = new Thickness(1),
-                    Focusable = false
-                };
-                btn.SetValue(Grid.ColumnProperty, 1);
-                btn.SetValue(Grid.RowProperty, 0);
-                btn.Click += (o, e) =>
-                {
-                    var qty = Item.GetQuantity(SupplierIndex);
-
-                    Parent.LoadJobData(qty.JobTotals.Keys);
-
-                    var items = qty.JobTotals.Select(x =>
-                    {
-                        var item = new StockForecastOrderingJobItem
-                        {
-                            JobID = x.Key,
-                            RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key),
-                            Quantity = x.Value
-                        };
-                        if(item.JobID == Guid.Empty)
-                        {
-                            item.Job = "General Stock";
-                        }
-                        else if(Parent.JobDetails.TryGetValue(item.JobID, out var job))
-                        {
-                            item.Job = $"{job.JobNumber}: {job.Name}";
-                        }
-                        return item;
-                    }).ToList();
-
-                    var window = new StockForecastOrderJobScreen();
-                    window.Items = items;
-                    if(window.ShowDialog() == true)
-                    {
-                        foreach(var item in items)
-                        {
-                            qty.JobTotals[item.JobID] = item.Quantity;
-                        }
-                        qty.DoChanged();
-                        editor.Text = string.Format("{0:F2}", Item.GetQuantity(SupplierIndex).JobTotal);
-                    }
-                };
-                grid.Children.Add(btn);
-
-                Content = grid;
-            }
+                Item.GetQuantity(SupplierIndex).Total = editor.Value ?? default;
+            };
+            Content = editor;
         }
     }
 
@@ -652,27 +739,57 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
 
     private void InitialiseSupplierColumn(int idx)
     {
-        var supplierProducts = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID).ToArray();
-
         var contextMenuFunc = (CoreRow[]? rows) =>
         {
             var row = rows?.FirstOrDefault();
             if (row is null) return null;
 
             var item = LoadItem(row);
-            var supplierProduct = GetSupplierProduct(item, Suppliers[idx].ID);
-            if (supplierProduct is not null)
-            {
-                return null;
-            }
 
             var menu = new ContextMenu();
             menu.AddItem("Create Supplier Product", null, new Tuple<StockForecastOrderingItem, int>(item, idx), CreateSupplierProduct_Click);
             return menu;
         };
 
-        // Making local copy of index so that the lambda can use it, and not the changed value of 'i'.
         var qtyColumn = new Tuple<DynamicActionColumn, QuantityControl?>(null!, null);
+        SupplierProductColumns[idx] = new DynamicTemplateColumn(row =>
+        {
+            var instance = LoadItem(row);
+
+            var comboBox = new ComboBox();
+
+            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}"))
+                .ToArray();
+            comboBox.SelectedValuePath = "Key";
+
+            comboBox.ItemsSource = items;
+            comboBox.DisplayMemberPath = "Value";
+
+            var qty = instance.GetQuantity(idx);
+
+            comboBox.Bind(ComboBox.SelectedValueProperty, qty, x => x.SupplierProduct);
+            comboBox.SelectionChanged += (o, e) =>
+            {
+                instance.CustomStrategy = true;
+                InvalidateRow(row);
+            };
+
+            comboBox.VerticalContentAlignment = VerticalAlignment.Center;
+            comboBox.Margin = new Thickness(2);
+            if(items.Length == 0)
+            {
+                comboBox.IsEnabled = false;
+            }
+
+            return comboBox;
+        })
+        {
+            HeaderText = "Supplier Product.",
+            Width = 80
+        };
         QuantityColumns[idx] = new DynamicTemplateColumn(row =>
         {
             var instance = LoadItem(row);
@@ -693,13 +810,10 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
             }
 
             var instance = LoadItem(row);
-            var qty = OrderType == StockForecastOrderingType.StockOrder
-                ? instance.GetQuantity(idx).StockTotal
-                : instance.GetQuantity(idx).JobTotal;
-            var supplierProduct = GetSupplierProduct(instance, Suppliers[idx].ID);
-            if(supplierProduct is not null)
+            var qty = instance.GetQuantity(idx);//.Total;
+            if(qty.SupplierProduct is not null)
             {
-                return $"{qty * supplierProduct.CostPrice:C2}";
+                return $"{qty.Total * qty.SupplierProduct.CostPrice:C2}";
             }
             else
             {
@@ -721,6 +835,7 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                 return summary;
             }
         };
+        ActionColumns.Add(SupplierProductColumns[idx]);
         ActionColumns.Add(QuantityColumns[idx]);
         ActionColumns.Add(CostColumns[idx]);
     }
@@ -738,48 +853,15 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
         if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
         {
             SupplierProducts.Add(supplierProduct);
+            var qty = item.GetQuantity(supplierIdx);
+            if(qty.SupplierProduct is null)
+            {
+                CalculateSupplierProduct(item, supplierIdx);
+            }
             InvalidateGrid();
         }
     }
 
-    private static bool Matches(StockForecastOrderingItem item, SupplierProduct supplierProduct)
-    {
-        return item.Product.ID == supplierProduct.Product.ID
-            && item.Style.ID == supplierProduct.Style.ID
-            && item.Dimensions.Equals(supplierProduct.Dimensions);
-    }
-    private static bool Matches(ProductInstance instance, SupplierProduct supplierProduct)
-    {
-        return instance.Product.ID == supplierProduct.Product.ID
-            && instance.Style.ID == supplierProduct.Style.ID
-            && instance.Dimensions.Equals(supplierProduct.Dimensions);
-    }
-
-    private SupplierProduct? GetSupplierProduct(StockForecastOrderingItem item)
-    {
-        var defaultSupplierProduct = SupplierProducts.FirstOrDefault(x => x.ID == item.Product.Supplier.ID);
-        if(defaultSupplierProduct is not null && Matches(item, defaultSupplierProduct))
-        {
-            return defaultSupplierProduct;
-        }
-        else
-        {
-            return SupplierProducts.FirstOrDefault(x => Matches(item, x));
-        }
-    }
-    private SupplierProduct? GetSupplierProduct(ProductInstance instance, Guid supplierID)
-    {
-        return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(instance, x));
-    }
-    private SupplierProduct? GetSupplierProduct(StockForecastOrderingItem item, Guid supplierID)
-    {
-        return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x));
-    }
-    //private double GetQuantity(SupplierProduct product)
-    //{
-    //    var instance = ProductInstances.WithIndex().Where(x => x.Value.Product.ID == product.ID)
-    //}
-
     private class CostAggregate : ISummaryAggregate
     {
         public double Sum { get; private set; }
@@ -808,11 +890,11 @@ public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrder
                 {
                     var rowIdx = dataRow.Row.Table.Rows.IndexOf(dataRow.Row);
                     var item = Grid.LoadItem(Grid.Data.Rows[rowIdx]);
-                    var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[SupplierIndex].ID);
-                    if(supplierProduct is not null)
+
+                    var qty = item.GetQuantity(SupplierIndex);
+                    if(qty.SupplierProduct is not null)
                     {
-                        var qty = item.GetQuantity(SupplierIndex);
-                        Sum += qty.GetTotal(Grid.OrderType) * supplierProduct.CostPrice;
+                        Sum += qty.Total * qty.SupplierProduct.CostPrice;
                     }
                 }
             }

+ 0 - 83
prs.desktop/Panels/Stock Forecast/OrderScreen/StockForecastOrderingJobGrid.cs

@@ -1,83 +0,0 @@
-using Comal.Classes;
-using InABox.Core;
-using InABox.DynamicGrid;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Media;
-
-namespace PRSDesktop.Panels.StockForecast.OrderScreen;
-
-public class StockForecastOrderingJobItem : BaseObject
-{
-    [EditorSequence(1)]
-    public string Job { get; set; }
-
-    [NullEditor]
-    public Guid JobID { get; set; }
-
-    [EditorSequence(2)]
-    [DoubleEditor]
-    public double RequiredQuantity { get; set; }
-
-    [EditorSequence(2)]
-    [DoubleEditor]
-    public double Quantity { get; set; }
-}
-
-
-public class StockForecastOrderingJobGrid : DynamicItemsListGrid<StockForecastOrderingJobItem>
-{
-    protected override void DoReconfigure(DynamicGridOptions options)
-    {
-        base.DoReconfigure(options);
-
-        options.Clear();
-        options.DirectEdit = true;
-    }
-
-    private class UIComponent : DynamicGridGridUIComponent<StockForecastOrderingJobItem>
-    {
-        public StockForecastOrderingJobGrid Grid { get; set; }
-
-        public UIComponent(StockForecastOrderingJobGrid grid)
-        {
-            Grid = grid;
-            Parent = grid;
-        }
-
-        protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
-        {
-            return base.GetCellBackground(row, column);
-        }
-    }
-    protected override IDynamicGridUIComponent<StockForecastOrderingJobItem> CreateUIComponent()
-    {
-        return new UIComponent(this);
-    }
-
-    protected override DynamicGridColumns LoadColumns()
-    {
-        var columns = new DynamicGridColumns();
-        columns.Add<StockForecastOrderingJobItem, string>(x => x.Job, 0, "Job", "", Alignment.MiddleLeft);
-        columns.Add<StockForecastOrderingJobItem, double>(x => x.RequiredQuantity, 70, "Req. Qty.", "F2", Alignment.MiddleCenter);
-        columns.Add<StockForecastOrderingJobItem, double>(x => x.Quantity, 70, "Qty.", "F2", Alignment.MiddleCenter);
-        return columns;
-    }
-
-    protected override void CustomiseEditor(StockForecastOrderingJobItem[] items, DynamicGridColumn column, BaseEditor editor)
-    {
-        base.CustomiseEditor(items, column, editor);
-
-        if(new Column<StockForecastOrderingJobItem>(x => x.Quantity).IsEqualTo(column.ColumnName))
-        {
-            column.Editor.Editable = Editable.Enabled;
-        }
-        else
-        {
-            column.Editor.Editable = Editable.Disabled;
-        }
-    }
-}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 428 - 415
prs.desktop/Panels/Stock Forecast/StockForecastGrid.cs


+ 117 - 0
prs.shared/Database Update Scripts/Update_8_14.cs

@@ -0,0 +1,117 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.Database;
+
+namespace PRS.Shared.Database_Update_Scripts;
+
+internal class Update_8_14 : DatabaseUpdateScript
+{
+
+    private static readonly int CHUNK_SIZE = 500;
+    private static readonly bool DESTRUCTIVE = false;
+    
+    public override VersionNumber Version => new(8, 14);
+    
+    public override bool Update()
+    {
+        var _provider = DbFactory.NewProvider(Logger.Main);
+        Clear_POIAs(_provider);
+        var _poias = Migrate_JRIPOIs(_provider);
+        Migrate_POIs(_provider, _poias);
+        return true;
+    }
+
+    private void Clear_POIAs(IProvider provider)
+    {
+        return;
+        Logger.Send(LogType.Information, "", "Clearing Existing Allocations");
+        var _queue = provider.Query(
+            new Filter<PurchaseOrderItemAllocation>().All(),
+            Columns.None<PurchaseOrderItemAllocation>().Add(x => x.ID)
+        ).Rows.ToQueue();
+
+        while (_queue.Any())
+        {
+            List<PurchaseOrderItemAllocation> _poias = _queue
+                .Dequeue(CHUNK_SIZE)
+                .Select(x => x.ToObject<PurchaseOrderItemAllocation>())
+                .ToList();
+            provider.Delete(_poias, "");
+
+            Logger.Send(LogType.Information, "", $"- Deleted {_poias.Count} Allocations ({_queue.Count} remaining)");
+        }
+    }
+
+    private List<PurchaseOrderItemAllocation> Migrate_JRIPOIs(IProvider provider)
+    {
+        var _result = new List<PurchaseOrderItemAllocation>();
+        return _result;
+        
+        Logger.Send(LogType.Information,"","Migrating JobRequisitionItems");
+        var _queue = provider.Query(
+            new Filter<JobRequisitionItemPurchaseOrderItem>().All(),
+            Columns.None<JobRequisitionItemPurchaseOrderItem>()
+                .Add(x=>x.PurchaseOrderItem.ID)
+                .Add(x=>x.PurchaseOrderItem.Qty)
+                .Add(x=>x.JobRequisitionItem.ID)
+                .Add(x=>x.JobRequisitionItem.Job.ID)
+        ).Rows.ToQueue();
+        
+        while (_queue.Any())
+        {
+            List<PurchaseOrderItemAllocation> _poias = new();
+            var _jripois = _queue.Dequeue(CHUNK_SIZE).Select(x=>x.ToObject<JobRequisitionItemPurchaseOrderItem>()).ToList();
+            foreach (var _jripoi in _jripois)
+            {
+                var _poia = new PurchaseOrderItemAllocation();
+                _poia.Item.ID = _jripoi.ID;
+                _poia.Job.ID = _jripoi.JobRequisitionItem.Job.ID;
+                _poia.JobRequisitionItem.ID = _jripoi.JobRequisitionItem.ID;
+                _poia.Quantity = _jripoi.PurchaseOrderItem.Qty;
+                _poias.Add(_poia);
+                CoreUtils.SetPropertyValue(_jripoi,"Job.ID",Guid.Empty);
+            }
+            provider.Save(_poias);
+            if (DESTRUCTIVE)
+                provider.Delete(_jripois,"");
+            Logger.Send(LogType.Information, "", $"- Created {_poias.Count} Allocations ({_queue.Count} remaining)");
+            _result.AddRange(_poias);
+        }
+
+        return _result;
+    }
+    
+    private void Migrate_POIs(IProvider provider, List<PurchaseOrderItemAllocation> poias)
+    {
+        Logger.Send(LogType.Information,"","Migrating PurchaseOrderItems");
+        var _ids = poias.Select(x => x.Item.ID).Distinct().ToArray();
+        var _queue = provider.Query(
+            new Filter<PurchaseOrderItem>("Job.ID").IsNotEqualTo(Guid.Empty),
+            Columns.Required<PurchaseOrderItem>().Add("Job.ID")
+        ).Rows.ToQueue();
+        
+        while (_queue.Any())
+        {
+            List<PurchaseOrderItemAllocation> _poias = new();
+            var _pois = _queue.Dequeue(CHUNK_SIZE)
+                .Where(r => !_ids.Contains(r.Get<PurchaseOrderItemAllocation,Guid>(c=>c.ID)))
+                .Select(x=>x.ToObject<PurchaseOrderItem>())
+                .ToList();
+            foreach (var _poi in _pois)
+            {
+                var _poia = new PurchaseOrderItemAllocation();
+                _poia.Item.ID = _poi.ID;
+                _poia.Job.ID = (Guid)(CoreUtils.GetPropertyValue(_poi, "Job.ID") ?? Guid.Empty);
+                _poia.Quantity = _poi.Qty;
+                _poias.Add(_poia);
+                CoreUtils.SetPropertyValue(_poi,"Job.ID",Guid.Empty);
+            }
+            provider.Save(_poias);
+            if(DESTRUCTIVE)
+                provider.Save(_pois);
+            Logger.Send(LogType.Information, "", $"- Created {_poias.Count} Allocations ({_queue.Count} remaining)");
+        }
+    }
+
+
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio