using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.DynamicGrid; using InABox.Wpf; using InABox.WPF; using NPOI.SS.Formula.Functions; using Syncfusion.Data; using Syncfusion.Data.Extensions; using Syncfusion.UI.Xaml.Grid; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using Columns = InABox.Core.Columns; using PRSDesktop.Panels.StockForecast.OrderScreen; using PRSDimensionUtils; using Syncfusion.Windows.Shared; namespace PRSDesktop; public class StockForecastBreakupKey(Guid jobID, Guid requiID) { public Guid JobID { get; set; } = jobID; public Guid RequiID { get; set; } = requiID; public override bool Equals(object? obj) { return obj is StockForecastBreakupKey key && key.JobID == JobID && key.RequiID == RequiID; } public override int GetHashCode() { return HashCode.Combine(JobID, RequiID); } } 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; } public class QuantityBreakup(Guid jobID, Guid requiID, string description, double qty) { // This may be blank, in which case this refers to General Stock. public Guid JobID { get; set; } = jobID; public Guid JobRequiItemID { get; set; } = requiID; public string Description { get; set; } = description; public double Quantity { get; set; } = qty; } private List RequiredQuantities { get; set; } = []; public List GetRequiredQuantities() => RequiredQuantities; public void SetRequiredQuantity(Guid jobID, Guid requiID, string description, double qty) { RequiredQuantities.Add(new(jobID, requiID, description,qty)); } } public enum StockForecastOrderingType { StockOrder, Breakup } public class StockForecastOrderingItemQuantity { public event Action? Changed; private double orderTotal; public double OrderTotal { get => orderTotal; set { orderTotal = value; Changed?.Invoke(); } } public Dictionary Breakups { get; init; } = []; private SupplierProduct? _supplierProduct; /// /// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column. /// public SupplierProduct? SupplierProduct { get => _supplierProduct; set { _supplierProduct = value; Changed?.Invoke(); } } public void DoChanged() { Changed?.Invoke(); } public double JobTotal => Breakups.Values.Sum(); public double GetTotal(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder ? OrderTotal : JobTotal; } public class StockOrderingItem : BaseObject { [EditorSequence(1)] public ProductLink Product { get; set; } [EditorSequence(2)] public ProductStyleLink Style { get; set; } [EditorSequence(3)] public StockDimensions Dimensions { get; set; } [EditorSequence(5)] [DoubleEditor] public double RequiredQuantity { get; set; } [EditorSequence(6)] [EnumLookupEditor(typeof(SupplierProductOrderStrategy))] public SupplierProductOrderStrategy OrderStrategy { get; set; } private Dictionary JobRequiredQuantities { get; set; } = new() { { new(Guid.Empty, Guid.Empty), 0.0 } }; public Dictionary GetJobRequiredQuantities() { return JobRequiredQuantities; } public void SetJobRequiredQuantity(Guid jobID, Guid requiID, double requiredQuantity) { JobRequiredQuantities[new(jobID, requiID)] = requiredQuantity; } public bool CustomStrategy { get; set; } = false; public StockForecastOrderingItemQuantity[] Quantities = []; public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i]; public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder ? Quantities.Sum(x => x.OrderTotal) : Quantities.Sum(x => x.JobTotal); public double GetRequiredQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder ? RequiredQuantity : JobRequiredQuantities.Values.Sum(); public void SetQuantities(StockForecastOrderingItemQuantity[] quantities) { Quantities = quantities; } } public class StockForecastOrderingResult( SupplierLink supplier, List breakups, StockOrderingItem item, double quantity, SupplierProduct supplierProduct) { public SupplierLink Supplier { get; set; } = supplier; public StockOrderingItem Item { get; set; } = item; public SupplierProduct SupplierProduct { get; set; } = supplierProduct; public double Quantity { get; set; } = quantity; public List Breakups { get; set; } = breakups; } public enum StockForecastOrderingStrategy { PerProduct, Exact, RoundUp, LowestUnitPrice, LowestOverallPrice, LowestOverstock } public class StockForecastOrderingGrid : DynamicItemsListGrid, ISpecificGrid { #region Internal Data + Caches private List SupplierProducts = []; private SupplierLink[] Suppliers = []; private DynamicActionColumn[] SupplierProductColumns = []; private DynamicActionColumn[] QuantityColumns = []; private DynamicActionColumn[] CostColumns = []; private readonly Dictionary JobDetails = []; private readonly Dictionary JobRequiDetails = []; #endregion private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage(); #region Public Properties public IList OrderData { get; set; } private StockForecastOrderingType _orderType = StockForecastOrderingType.Breakup; public StockForecastOrderingType OrderType { get => _orderType; set { if(_orderType != value) { _orderType = value; if (OrderData != null) { CalculateQuantities(true); UIComponent.UpdateOrderType(OrderType); foreach(var control in QuantityControls) { control.UpdateControl(OrderType); } Refresh(true, true); } } } } private StockForecastOrderingStrategy orderStrategy; public StockForecastOrderingStrategy OrderStrategy { get => orderStrategy; set { orderStrategy = value; if (OrderData != null) { foreach (var item in Items) { item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(value, item.Product.OrderStrategy); item.CustomStrategy = false; } CalculateQuantities(false); Refresh(false, true); } } } public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType)); public IEnumerable Results { get { for(int i = 0; i < Suppliers.Length; ++i) { var supplier = Suppliers[i]; foreach(var item in Items) { var qty = item.GetQuantity(i); if (qty.SupplierProduct is null) { continue; } if(OrderType == StockForecastOrderingType.StockOrder && qty.OrderTotal > 0) { yield return new(supplier, new(), item, qty.OrderTotal, qty.SupplierProduct); } else if(qty.OrderTotal > 0) { var breakups = new List(); foreach(var (key, q) in qty.Breakups) { // Check JobID because we are to skip the empty job (this is just the difference between all the allocations and the quantity on the PO). if(q > 0 && key.JobID != Guid.Empty) { breakups.Add(new(key.JobID, key.RequiID, "", q)); } } yield return new(supplier, breakups, item, qty.OrderTotal, qty.SupplierProduct); } } } } } #endregion public StockForecastOrderingGrid() { HiddenColumns.Add(x => x.Product.Image.ID); } private static SupplierProductOrderStrategy CastOrderStrategyToProductOrderStrategy(StockForecastOrderingStrategy strategy, SupplierProductOrderStrategy defaultValue) { return strategy switch { StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact, StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy .LowestOverallPrice, StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice, StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock, StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp, StockForecastOrderingStrategy.PerProduct or _ => defaultValue }; } #region UI Component private Component? _uiComponent; private Component UIComponent { get { _uiComponent ??= new Component(this); return _uiComponent; } } protected override IDynamicGridUIComponent CreateUIComponent() { return UIComponent; } private class Component : DynamicGridGridUIComponent { private StockForecastOrderingGrid Grid; public Component(StockForecastOrderingGrid grid) { Parent = grid; Grid = grid; UpdateOrderType(grid.OrderType); } public void UpdateOrderType(StockForecastOrderingType type) { DataGrid.FrozenColumnCount = 8; } protected override Brush? GetCellSelectionBackgroundBrush() { return null; } protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column) { var item = Grid.LoadItem(row); if(column is DynamicActionColumn ac) { var qIdx = Grid.QuantityColumns.IndexOf(ac); var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac)); if(idx != -1) { var supplierProduct = item.GetQuantity(idx).SupplierProduct; return supplierProduct is null ? Brushes.Gainsboro : Brushes.WhiteSmoke; } } return base.GetCellBackground(row, column); } } #endregion private bool _observing = true; private void SetObserving(bool observing) { _observing = observing; } protected override void Changed() { if (_observing) { base.Changed(); } } protected override void DoReconfigure(DynamicGridOptions options) { options.Clear(); options.FilterRows = true; } private bool _loadedData = false; private void LoadData() { var supplierProductColumns = Columns.None().Add(x => x.ID) .Add(x => x.SupplierLink.ID) .Add(x => x.Product.ID) .Add(x => x.Style.ID) .Add(x => x.Style.Code) .Add(x => x.Job.ID) .Add(x => x.Job.JobNumber) .Add(x => x.ForeignCurrencyPrice) .Add(x => x.CostPrice) .AddDimensionsColumns(x => x.Dimensions) .Add(x => x.SupplierLink.Code); SupplierProducts = Client.Query( new Filter(x => x.Product.ID).InList(OrderData.Select(x => x.Product.ID).ToArray()) .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty), supplierProductColumns, new SortOrder(x => x.SupplierLink.Code)) .ToList(); Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray(); CalculateQuantities(true); _loadedData = true; } private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx) { var qty = new StockForecastOrderingItemQuantity(); qty.Changed += () => { if (!_observing) return; var row = Data.Rows[itemIdx]; InvalidateRow(row); DoChanged(); }; return qty; } private SupplierProduct? CalculateSupplierProduct(StockOrderingItem item, int supplierIdx) { var supplierProducts = string.IsNullOrWhiteSpace(item.Dimensions.Unit.Conversion) ? SupplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions)) : SupplierProducts; 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.OrderTotal = 0; qty.Breakups.Clear(); foreach(var id in item.GetJobRequiredQuantities().Keys) { qty.Breakups[id] = 0; } return supplierProduct; } private void SetSupplierProduct(StockOrderingItem item, SupplierProduct supplierProduct) { var supplierIdx = Suppliers.WithIndex() .FirstOrDefault(x => x.Value.ID == supplierProduct.SupplierLink.ID, new KeyValuePair(-1, null)).Key; if(supplierIdx != -1) { var qty = item.GetQuantity(supplierIdx); if(OrderType == StockForecastOrderingType.Breakup) { qty.Breakups.Clear(); foreach(var (id, q) in item.GetJobRequiredQuantities()) { qty.Breakups[id] = q; } } qty.OrderTotal = GetRequiredQuantity(item, supplierProduct); } } private void CalculateSupplierProduct(StockOrderingItem item) { var selectedSupplierProducts = new List(); for(int i = 0; i < Suppliers.Length; ++i) { var supplierProduct = CalculateSupplierProduct(item, i); if(supplierProduct is not null) { selectedSupplierProducts.Add(supplierProduct); } } var selectedSupplierProduct = SelectSupplierProduct(selectedSupplierProducts, item); if(selectedSupplierProduct is not null) { SetSupplierProduct(item, selectedSupplierProduct); } } private void CalculateQuantities(bool recreateItems) { SetObserving(false); if (recreateItems) { Items.Clear(); foreach(var dataItem in OrderData) { var item = new StockOrderingItem(); item.Product.CopyFrom(dataItem.Product); item.Style.CopyFrom(dataItem.Style); item.Dimensions.CopyFrom(dataItem.Dimensions); item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy); item.RequiredQuantity = dataItem.RequiredQuantity; foreach(var breakup in dataItem.GetRequiredQuantities()) { item.SetJobRequiredQuantity(breakup.JobID, breakup.JobRequiItemID, breakup.Quantity); } 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(); } #region Order Strategy private double CalculateSupplierProductRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct) { var supplierIdx = Suppliers.WithIndex().FirstOrDefault(x => x.Value.ID == supplierProduct.SupplierLink.ID).Key; var qty = item.GetQuantity(supplierIdx); var req = OrderType == StockForecastOrderingType.StockOrder ? item.RequiredQuantity : qty.Breakups.Sum(x => x.Value); var d = new StockDimensions(); d.CopyFrom(supplierProduct.Dimensions); var result = DimensionUtils.ConvertDimensions(d, 1.0, (f,c) => Client.Query(f,c)); req = Math.Ceiling(req / result); return req; } private SupplierProduct? SelectSupplierProduct(IEnumerable supplierProducts, StockOrderingItem item) { double DimensionsRatio(SupplierProduct x) { return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value); } switch (item.OrderStrategy) { case SupplierProductOrderStrategy.Exact: case SupplierProductOrderStrategy.RoundUp: return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions)) .MinBy(x => x.CostPrice); case SupplierProductOrderStrategy.LowestOverallPrice: return supplierProducts.MinBy(x => x.CostPrice * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x))); case SupplierProductOrderStrategy.LowestUnitPrice: return supplierProducts.MinBy(x => x.CostPrice * DimensionsRatio(x)); case SupplierProductOrderStrategy.LowestOverstock: return supplierProducts.MinBy(x => x.Dimensions.Value * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x))); default: return null; } } private double GetRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct) { var requiredQuantity = CalculateSupplierProductRequiredQuantity(item, supplierProduct); double DimensionsRatio(SupplierProduct x) { return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value); } var _strategy = item.CustomStrategy ? SupplierProductOrderStrategy.LowestOverstock : item.OrderStrategy; switch (_strategy) { case SupplierProductOrderStrategy.Exact: return requiredQuantity; case SupplierProductOrderStrategy.RoundUp: return Math.Ceiling(requiredQuantity); case SupplierProductOrderStrategy.LowestOverallPrice: case SupplierProductOrderStrategy.LowestUnitPrice: case SupplierProductOrderStrategy.LowestOverstock: return Math.Ceiling(requiredQuantity * DimensionsRatio(supplierProduct)); default: return 0.0; } } #endregion private bool _loadedColumns = false; protected override DynamicGridColumns LoadColumns() { if (!_loadedData) { LoadData(); } var columns = new DynamicGridColumns(); columns.Add(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter); columns.Add(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft); columns.Add(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter); columns.Add(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter); columns.Add(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter); if (!_loadedColumns) { ActionColumns.Clear(); ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicImagePreviewColumn(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicTemplateColumn(row => { var item = LoadItem(row); var box = new ComboBox(); box.ItemsSource = Enum.GetValues() .Select(x => new KeyValuePair(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 = 140 }); 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; } private void EditSupplierProductGrid(DynamicGrid grid) { grid.OnCustomiseEditor += (sender, items, column, editor) => { if(new Column(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName) || new Column(x => x.Product.ID).IsEqualTo(column.ColumnName) || new Column(x => x.Style.ID).IsEqualTo(column.ColumnName)) { editor.Editable = editor.Editable.Combine(Editable.Disabled); } }; } private void BuildMenu(DynamicMenuColumn column, CoreRow? row) { if (row is null) return; column.AddItem("New Supplier", null, row => { if (row is null) return; var selection = new MultiSelectDialog( new Filter(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()), Columns.None().Add(x => x.ID).Add(x => x.Code), multiselect: false); if (selection.ShowDialog() != true) { return; } var supplier = selection.Data().Rows.First().ToObject(); var orderingItem = LoadItem(row); var supplierProduct = new SupplierProduct(); supplierProduct.Product.CopyFrom(orderingItem.Product); supplierProduct.Style.CopyFrom(orderingItem.Style); supplierProduct.Dimensions.CopyFrom(orderingItem.Dimensions); supplierProduct.SupplierLink.CopyFrom(supplier); if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid)) { SupplierProducts.Add(supplierProduct); var newSuppliers = new SupplierLink[Suppliers.Length + 1]; var newIdx = Suppliers.Length; for (int i = 0; i < Suppliers.Length; i++) { newSuppliers[i] = Suppliers[i]; } newSuppliers[newIdx] = supplierProduct.SupplierLink; foreach (var (itemIdx, item) in Items.WithIndex()) { var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length]; for (int i = 0; i < Suppliers.Length; ++i) { quantities[i] = item.GetQuantity(i); } var newQty = CreateQuantity(itemIdx); quantities[newIdx] = newQty; if(OrderType == StockForecastOrderingType.StockOrder) { newQty.OrderTotal = 0; } else { newQty.OrderTotal = 0; foreach(var id in item.GetJobRequiredQuantities().Keys) { newQty.Breakups[id] = 0; } } item.SetQuantities(quantities); } Suppliers = newSuppliers; foreach (var item in Items) { CalculateSupplierProduct(item, newIdx); } _loadedColumns = false; Refresh(true, true); } }); } private BitmapImage? Warning_Image(CoreRow? row) { if (row is null) return _warning; var item = LoadItem(row); if(item.GetTotalQuantity(OrderType) < item.RequiredQuantity) { return _warning; } else { return null; } } protected override void ConfigureColumnGroups() { for(int idx = 0; idx < Suppliers.Length; ++idx) { GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]); } } #region Job Data Cache private void LoadJobData(IEnumerable ids) { var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray(); if(neededIDs.Length > 0) { var details = Client.Query( new Filter(x => x.ID).InList(neededIDs), Columns.None().Add(x => x.ID) .Add(x => x.JobNumber) .Add(x => x.Name)); foreach(var job in details.ToObjects()) { JobDetails[job.ID] = job; } } } private void LoadJobRequiData(IEnumerable ids) { var neededIDs = ids.Where(x => x != Guid.Empty && !JobRequiDetails.ContainsKey(x)).ToArray(); if(neededIDs.Length > 0) { var details = Client.Query( new Filter(x => x.ID).InList(neededIDs), Columns.None().Add(x => x.ID) .Add(x => x.Requisition.Number) .Add(x => x.Requisition.Description)); foreach(var requi in details.ToObjects()) { JobRequiDetails[requi.ID] = requi; } } } #endregion private class QuantityControl : ContentControl { private readonly StockOrderingItem Item; private readonly int SupplierIndex; private readonly StockForecastOrderingGrid Parent; public QuantityControl(StockForecastOrderingGrid parent, StockOrderingItem item, int supplierIndex, StockForecastOrderingType mode) { Parent = parent; Item = item; SupplierIndex = supplierIndex; UpdateControl(mode); } public void UpdateControl(StockForecastOrderingType mode) { // If no supplier product has been selected for this cell, we can't allow the user to select a quantity. var supplierProduct = Item.GetQuantity(SupplierIndex).SupplierProduct; if(supplierProduct is null) { Content = null; return; } if(mode == StockForecastOrderingType.StockOrder) { 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).OrderTotal }; editor.ValueChanged += (o, e) => { Item.GetQuantity(SupplierIndex).OrderTotal = editor.Value ?? default; }; Content = editor; } else if(mode == StockForecastOrderingType.Breakup) { 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 _breakups = Item.GetQuantity(SupplierIndex).Breakups.Sum(x => x.Value); var editor = new DoubleTextBox { VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Center, HorizontalContentAlignment = HorizontalAlignment.Center, Background = new SolidColorBrush(Colors.WhiteSmoke), BorderThickness = new Thickness(0.0), Value = Item.GetQuantity(SupplierIndex).OrderTotal, IsReadOnly = true, Focusable = false, }; 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.Breakups.Keys.Select(x => x.JobID)); Parent.LoadJobRequiData(qty.Breakups.Keys.Select(x => x.RequiID)); var items = qty.Breakups.Select(x => { var item = new StockForecastOrderingJobItem { JobID = x.Key.JobID, JobRequiID = x.Key.RequiID, RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key), Quantity = x.Value }; if(item.JobID == Guid.Empty) { item.Description = "General Stock"; } else if(Parent.JobDetails.TryGetValue(item.JobID, out var job)) { if(Parent.JobRequiDetails.TryGetValue(item.JobRequiID, out var requi)) { item.Description = $"{job.JobNumber}: Requi #{requi.Requisition.Number} ({requi.Requisition.Description})"; } else { item.Description = $"{job.JobNumber}: {job.Name}"; } } return item; }).ToList(); var genitem = items.FirstOrDefault(x => x.JobID == Guid.Empty && x.JobRequiID == Guid.Empty); if (genitem == null) { genitem = new StockForecastOrderingJobItem { Description = "General Stock", JobID = Guid.Empty, JobRequiID = Guid.Empty }; items.Insert(0, genitem); } else { items.Remove(genitem); items.Insert(0, genitem); } var window = new StockForecastOrderJobScreen(); window.Items = items; if(window.ShowDialog() == true) { foreach(var item in items) { qty.Breakups[new(item.JobID, item.JobRequiID)] = item.Quantity; } qty.DoChanged(); var req = Parent.GetRequiredQuantity(Item, supplierProduct); editor.Value = req; qty.OrderTotal = req; } }; grid.Children.Add(btn); Content = grid; } } } private List QuantityControls = []; private void InitialiseSupplierColumn(int idx) { var contextMenuFunc = (CoreRow[]? rows) => { var row = rows?.FirstOrDefault(); if (row is null) return null; var item = LoadItem(row); var menu = new ContextMenu(); menu.AddItem("Create Supplier Product", null, new Tuple(item, idx), CreateSupplierProduct_Click); return menu; }; var qtyColumn = new Tuple(null!, null); SupplierProductColumns[idx] = new DynamicTemplateColumn(row => { var instance = LoadItem(row); var comboBox = new ComboBox(); comboBox.Tag = idx; var supplierProducts = string.IsNullOrWhiteSpace(instance.Dimensions.Unit.Conversion) ? SupplierProducts.Where(x => x.Dimensions.Equals(instance.Dimensions)) : SupplierProducts; var items = supplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID) .Select(x => new KeyValuePair(x, x.Job.ID == Guid.Empty ? x.Dimensions.UnitSize : $"Job {x.Job.JobNumber}: {x.Dimensions.UnitSize}")); if (items.Any()) items = items.Prepend(new KeyValuePair(null, "")); comboBox.SelectedValuePath = "Key"; comboBox.ItemsSource = items.ToArray(); comboBox.DisplayMemberPath = "Value"; var qty = instance.GetQuantity(idx); comboBox.Bind(ComboBox.SelectedValueProperty, qty, x => x.SupplierProduct); comboBox.SelectionChanged += (o, e) => { var box = o as ComboBox; instance.CustomStrategy = true; var _item = LoadItem(row); var _product = ((o as ComboBox)?.SelectedValue as SupplierProduct ?? new SupplierProduct()); qty.OrderTotal = GetRequiredQuantity(_item, _product); InvalidateRow(row); }; comboBox.VerticalContentAlignment = VerticalAlignment.Center; comboBox.Margin = new Thickness(2); if(!items.Any()) comboBox.IsEnabled = false; return comboBox; }) { HeaderText = "U.O.M.", Width = 80 }; QuantityColumns[idx] = new DynamicTemplateColumn(row => { var instance = LoadItem(row); var control = new QuantityControl(this, instance, idx, OrderType); QuantityControls.Add(control); return control; }) { HeaderText = "Qty.", Width = 80, ContextMenu = contextMenuFunc }; CostColumns[idx] = new DynamicTextColumn(row => { if(row is null) { return "Cost"; } var instance = LoadItem(row); var qty = instance.GetQuantity(idx); if(qty.SupplierProduct is not null) { return $"{qty.OrderTotal * qty.SupplierProduct.CostPrice:C2}"; } else { return ""; } }) { HeaderText = "Cost", Width = 80, ContextMenu = contextMenuFunc, GetSummary = () => { return new DynamicGridCustomSummary((rows) => Cost_Aggregate(idx, rows), "C2"); } }; ActionColumns.Add(SupplierProductColumns[idx]); ActionColumns.Add(QuantityColumns[idx]); ActionColumns.Add(CostColumns[idx]); } private double Cost_Aggregate(int supplierIdx, IEnumerable rows) { return rows.Sum(row => { var item = LoadItem(row); var qty = item.GetQuantity(supplierIdx); if(qty.SupplierProduct is not null) { return qty.OrderTotal * qty.SupplierProduct.CostPrice; } else { return 0; } }); } private void CreateSupplierProduct_Click(Tuple tuple) { var (item, supplierIdx) = tuple; var supplierProduct = new SupplierProduct(); supplierProduct.Product.CopyFrom(item.Product); supplierProduct.Style.CopyFrom(item.Style); supplierProduct.Dimensions.CopyFrom(item.Dimensions); supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]); if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid)) { SupplierProducts.Add(supplierProduct); var qty = item.GetQuantity(supplierIdx); if(qty.SupplierProduct is null) { var hasSelected = item.Quantities.Any(x => x.SupplierProduct is not null); var sp = CalculateSupplierProduct(item, supplierIdx); if(!hasSelected && sp is not null) { SetSupplierProduct(item, sp); } } InvalidateGrid(); } } }