using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.DynamicGrid; using InABox.Wpf; using InABox.WPF; using Newtonsoft.Json; using PRSDesktop.Panels.StockForecast.OrderScreen; using Syncfusion.UI.Xaml.Diagram.Controls; using Syncfusion.Windows.PdfViewer; namespace PRSDesktop; public class StockForecastGrid : DynamicDataGrid, IDataModelSource { private enum ColumnTag { MinimumStockRequired, GeneralStockHoldings, GeneralPurchaseOrders, JobStockRequired, JobStockHoldings, JobPurchaseOrders, BalanceRequired } private CoreTable? _stockHoldings = null; private CoreTable? _poItems = null; private CoreTable? _jobBOMs = null; private CoreTable? _stockMovements = null; private CoreTable? _supplierProducts = null; private static readonly BitmapImage _warning = InABox.Wpf.Resources.warning.AsBitmapImage(); private static readonly BitmapImage _tick = InABox.Wpf.Resources.tick.AsBitmapImage(); private static readonly BitmapImage _cart = PRSDesktop.Resources.purchase.AsBitmapImage(); public Guid[] GroupIDs { get; set; } = []; public Guid[] JobIDs { get; set; } = []; public HashSet SupplierIDs { get; set; } = []; private readonly Button OrderButton; private HashSet SelectedForOrder = []; public StockForecastGrid() : base() { ColumnsTag = "StockForecastGrid"; HiddenColumns.Add(x=>x.ID); HiddenColumns.Add(x => x.Product.ID); HiddenColumns.Add(x => x.Product.Issues); HiddenColumns.Add(x => x.Style.ID); HiddenColumns.Add(x => x.Dimensions.UnitSize); HiddenColumns.Add(x => x.Product.Image.ID); HiddenColumns.Add(x => x.Product.Image.FileName); HiddenColumns.Add(x=>x.Product.Supplier.ID); HiddenColumns.Add(x=>x.Product.Supplier.SupplierLink.ID); HiddenColumns.Add(x=>x.MinimumStockLevel); ActionColumns.Add(new DynamicImageColumn(Issues_Image, null) { ToolTip = Issues_Tooltip, Position = DynamicActionColumnPosition.Start }); ActionColumns.Add(new DynamicImagePreviewColumn(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start }); CreateColumn(GetMinimumStockLevel, ColumnTag.MinimumStockRequired,"Min Stk.","F2"); CreateColumn(GetGeneralStockLevel, ColumnTag.GeneralStockHoldings,"Gen Hld.","F2"); CreateColumn(GetGeneralPurchaseOrder, ColumnTag.GeneralPurchaseOrders, "Gen PO.","F2"); CreateColumn(GetBOMBalance, ColumnTag.JobStockRequired, "Job BOM.","F2"); CreateColumn(GetReservedStock, ColumnTag.JobStockHoldings, "Job Hld.","F2"); CreateColumn(GetReservedPurchaseOrder, ColumnTag.JobPurchaseOrders, "Job PO.","F2"); CreateColumn(GetBalanceRequired, ColumnTag.BalanceRequired,"Required",""); ActionColumns.Add(new DynamicImageColumn(SelectForOrder_Image, SelectForOrder_Click) { Position = DynamicActionColumnPosition.End }); OrderButton = AddButton("Order Stock", _cart, OrderStock_Click); OrderButton.IsEnabled = false; } private BitmapImage? Issues_Image(CoreRow? row) { return (row is null) ? _warning : row.Get(x => x.Product.Issues).IsNullOrWhiteSpace() ? null : _warning; } private FrameworkElement? Issues_Tooltip(DynamicActionColumn column, CoreRow? row) { return (row is null) ? null : column.TextToolTip(row.Get(x => x.Product.Issues)); } #region UIComponent private UIComponent? _uicomponent = null; private class UIComponent : DynamicGridGridUIComponent { private StockForecastGrid Grid; public UIComponent(StockForecastGrid grid) { Grid = grid; Parent = grid; } private int? _instanceProductIDCol; private int? _instanceStyleIDCol; private int? _instanceUnitSizeCol; private int? _instanceSupplierCol; private int? _supplierProductIDCol; private int? _supplierStyleIDCol; private int? _supplierUnitSizeCol; private int? _supplierSupplierCol; public bool CheckSuppliers(CoreRow row) { if (Grid._supplierProducts == null) return false; _supplierProductIDCol ??= new Columns(Grid._supplierProducts.Columns).IndexOf(x => x.Product.ID); _supplierStyleIDCol ??= new Columns(Grid._supplierProducts.Columns).IndexOf(x => x.Style.ID); _supplierUnitSizeCol ??= new Columns(Grid._supplierProducts.Columns).IndexOf(x => x.Dimensions.UnitSize); _supplierSupplierCol ??= new Columns(Grid._supplierProducts.Columns).IndexOf(x => x.SupplierLink.ID); _instanceProductIDCol ??= new Columns(row.Table.Columns).IndexOf(x => x.Product.ID); _instanceStyleIDCol ??= new Columns(row.Table.Columns).IndexOf(x => x.Style.ID); _instanceUnitSizeCol ??= new Columns(row.Table.Columns).IndexOf(x => x.Dimensions.UnitSize); return Grid._supplierProducts.Rows.Any(r => Equals(r.Values[_supplierProductIDCol.Value], row.Values[_instanceProductIDCol.Value]) && Equals(r.Values[_supplierStyleIDCol.Value], row.Values[_instanceStyleIDCol.Value]) && Equals(r.Values[_supplierUnitSizeCol.Value], row.Values[_instanceUnitSizeCol.Value]) && Grid.SupplierIDs.Contains((Guid?)r.Values[_supplierSupplierCol.Value] ?? Guid.Empty)); } protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column) { _instanceProductIDCol ??= new Columns(row.Table.Columns).IndexOf(x => x.Product.ID); _instanceStyleIDCol ??= new Columns(row.Table.Columns).IndexOf(x => x.Style.ID); _instanceUnitSizeCol ??= new Columns(row.Table.Columns).IndexOf(x => x.Dimensions.UnitSize); if (column is DynamicTextColumn col && Grid._summaryinfo.TryGetValue(row.Get(x=>x.ID), out StockForecastInfo? info)) { var stock = Math.Max(0.0F, info.MinStock - (info.GenStock + info.GenPO)).IsEffectivelyEqual(0.0F) ? new SolidColorBrush(Colors.LightBlue) { Opacity = 0.5 } : new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 }; var job = Math.Max(0.0F, info.JobBOM - (info.JobStock + info.JobPO)).IsEffectivelyEqual(0.0F) ? new SolidColorBrush(Colors.LightGreen) { Opacity = 0.5 } : new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 }; var overall = !(Grid.Optimise ? info.Optimised : info.Required).IsEffectivelyEqual(0.0F) ? new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 } : null; return col.Tag switch { ColumnTag.MinimumStockRequired => stock, ColumnTag.GeneralStockHoldings => stock, ColumnTag.GeneralPurchaseOrders => stock, ColumnTag.JobStockRequired => job, ColumnTag.JobStockHoldings => job, ColumnTag.JobPurchaseOrders => job, ColumnTag.BalanceRequired => overall, _ => null }; } else { if (Grid.AllStock && !CheckSuppliers(row)) return new SolidColorBrush(Colors.Silver) { Opacity = 0.5F }; } return null; } } protected override IDynamicGridUIComponent CreateUIComponent() { return _uicomponent ??= new UIComponent(this); } #endregion protected override void DoReconfigure(FluentList options) { base.DoReconfigure(options); options .BeginUpdate() .Clear() .Add(DynamicGridOption.RecordCount) .Add(DynamicGridOption.SelectColumns) .Add(DynamicGridOption.FilterRows) .Add(DynamicGridOption.ExportData) .Add(DynamicGridOption.MultiSelect) .Add(DynamicGridOption.HideDatabaseFilters) .EndUpdate(); } protected override void ConfigureColumnGroups() { base.ConfigureColumnGroups(); AddColumnGrouping() .AddGroup("General Stock", GetColumn(ColumnTag.MinimumStockRequired), GetColumn(ColumnTag.GeneralPurchaseOrders)) .AddGroup("Job Stock", GetColumn(ColumnTag.JobStockRequired), GetColumn(ColumnTag.JobPurchaseOrders)); } public override DynamicGridColumns GenerateColumns() { 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.Style.Code, 120, "Style Code", "", Alignment.MiddleCenter); columns.Add(x => x.Dimensions.UnitSize, 120, "Unit Size", "", Alignment.MiddleCenter); return columns; } #region Column Data and Details private void CreateColumn(DynamicTextColumn.GetTextDelegate calculate, ColumnTag tag, string header, string format) { var column = new DynamicTextColumn(calculate) { Width = 60, Format=format, Position = DynamicActionColumnPosition.End, Tag = tag, HeaderText = header, Filters = [""], FilterRecord = (row, filters) => { if (filters.Length == 1 && filters[0].Length == 0) return true; var value = GetColumnCalculatedData(tag, row.Get(x => x.ID)); if(!value.HasValue) { return false; } else { return filters.Contains(value.Value.ToString("F2")); } } }; ActionColumns.Add(column); } private DynamicTextColumn GetColumn(ColumnTag tag) => (ActionColumns.First(x => Equals(x.Tag, tag)) as DynamicTextColumn)!; private object GetMinimumStockLevel(CoreRow? row) => row?.Get(x => x.MinimumStockLevel) ?? 0.0F; private object GetGeneralStockLevel(CoreRow? row) { if (row != null && _summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) return info.GenStock; return 0.0F; } private object GetGeneralPurchaseOrder(CoreRow? row) { if (row != null && _summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) return info.GenPO; return 0.0F; } private object GetBOMBalance(CoreRow? row) { if (row != null && _summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) return info.JobBOM; return 0.0F; } private object GetReservedStock(CoreRow? row) { if (row != null && _summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) return info.JobStock; return 0.0F; } private object GetReservedPurchaseOrder(CoreRow? row) { if (row != null && _summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) return info.JobPO; return 0.0F; } private object GetBalanceRequired(CoreRow? row) { if (row != null && _summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) return Optimise ? info.Optimised.IsEffectivelyEqual(0.0F) ? "" : $"{info.Optimised:F2}" : info.Required.IsEffectivelyEqual(0.0F) ? "" : $"{info.Required:F2}"; return ""; } private void ShowDetailGrid( String tag, Expression> productcol, Guid productid, Expression> stylecol, Guid? styleid, Expression> unitcol, String unitsize, Expression>? jobcol, Filter? extrafilter, Func? rowfilter ) { var grid = (Activator.CreateInstance(typeof(DynamicDataGrid<>).MakeGenericType(typeof(TEntity))) as IDynamicDataGrid); if (grid == null) { MessageWindow.ShowError($"Cannot create Grid for [{typeof(TEntity).Name}]", "", shouldLog: false); return; } grid.ColumnsTag = $"{ColumnsTag}.{tag}"; grid.Reconfigure(options => { options.BeginUpdate().Clear().AddRange(DynamicGridOption.FilterRows, DynamicGridOption.SelectColumns).EndUpdate(); }); grid.OnDefineFilter += t => { var filter = new Filter(productcol).IsEqualTo(productid) .And(unitcol).IsEqualTo(unitsize); if (styleid.HasValue) filter = filter.And(stylecol).IsEqualTo(styleid); if (jobcol != null) filter = filter.And(new Filter(jobcol).InList(JobIDs)); if (extrafilter != null) filter = filter.And(extrafilter); return filter; }; grid.OnFilterRecord += row => rowfilter?.Invoke(row) ?? true; var window = DynamicGridUtils.CreateGridWindow($"Viewing {CoreUtils.Neatify(tag)} Calculation", grid); window.ShowDialog(); } protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args) { //base.DoDoubleClick(sender, args); var productid = args?.Row?.Get(c => c.Product.ID) ?? Guid.Empty; var styleid = HasStyle() ? args?.Row?.Get(c => c.Style.ID) : null; var unitsize = args?.Row?.Get(c => c.Dimensions.UnitSize) ?? ""; if (Equals(args?.Column?.Tag, ColumnTag.GeneralStockHoldings)) { ShowDetailGrid( ColumnTag.GeneralStockHoldings.ToString(), x => x.Product.ID, productid, x => x.Style.ID, styleid, x=>x.Dimensions.UnitSize, unitsize, null, new Filter(x=>x.Job.ID).IsEqualTo(Guid.Empty), null ); } else if (Equals(args?.Column?.Tag, ColumnTag.GeneralPurchaseOrders)) { ShowDetailGrid( ColumnTag.GeneralPurchaseOrders.ToString(), x => x.Product.ID, productid, x => x.Style.ID, styleid, x=>x.Dimensions.UnitSize, unitsize, null, new Filter(x=>x.Job.ID).IsEqualTo(Guid.Empty) .And(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue), null ); } else if (Equals(args?.Column?.Tag, ColumnTag.JobStockRequired)) { ShowDetailGrid( ColumnTag.JobStockRequired.ToString(), x => x.Product.ID, productid, x => x.Style.ID, styleid, x=>x.Dimensions.UnitSize, unitsize, x => x.Job.ID, new Filter(x=>x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue), null ); } else if (Equals(args?.Column?.Tag, ColumnTag.JobStockHoldings)) { ShowDetailGrid( ColumnTag.JobStockHoldings.ToString(), x => x.Product.ID, productid, x => x.Style.ID, styleid, x=>x.Dimensions.UnitSize, unitsize, x => x.Job.ID, null, null ); } else if (Equals(args?.Column?.Tag, ColumnTag.JobPurchaseOrders)) { ShowDetailGrid( ColumnTag.GeneralPurchaseOrders.ToString(), x => x.Product.ID, productid, x => x.Style.ID, styleid, x=>x.Dimensions.UnitSize, unitsize, x=>x.Job.ID, new Filter(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue), null ); } } #endregion #region Refresh private bool HasStyle() { return DataColumns().ColumnNames().Any(x => x.StartsWith("Style.") && !x.Equals("Style.ID")); } private Tuple[] GetKeys(IEnumerable rows, Columns columns, bool hasstyle) { int productcol = columns.IndexOf(x => x.Product.ID); int stylecol = hasstyle ? columns.IndexOf(x => x.Style.ID) : -1; int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize); var result = rows.Select(r => new Tuple( (Guid)(r.Values[productcol] ?? Guid.Empty), (stylecol != -1) ? (Guid)(r.Values[stylecol] ?? Guid.Empty) : null, (String)(r.Values[unitcol] ?? "")) ).Distinct().ToArray(); return result; } private CoreRow[] GetRows(IEnumerable rows, Columns columns, Guid productid, Guid? styleid, string unitsize, Guid[] jobids) where TSource : IJobMaterial { int productcol = columns.IndexOf(x => x.Product.ID); int stylecol = styleid.HasValue ? columns.IndexOf(x => x.Style.ID) : -1; int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize); int jobcol = columns.IndexOf(x => x.Job.ID); var subset = rows .Where(r=> Guid.Equals(r.Values[productcol], productid) && (!styleid.HasValue || Guid.Equals(r.Values[stylecol], styleid)) && String.Equals(r.Values[unitcol], unitsize) && jobids.Any(x=>Equals(x,r.Values[jobcol])) ); return subset.ToArray(); } private double Aggregate(IEnumerable rows, Columns columns, bool hasstyle, bool hasjob, Expression> source, CoreRow? target = null, Expression>? aggregate = null) { int srcol = columns.IndexOf(source); if (srcol == -1) return 0.00; var total = rows.Aggregate(0d, (value, row) => value + (double)(row.Values[srcol] ?? 0.0d)); // int productcol = columns.IndexOf(x => x.Product.ID); // int stylecol = hasstyle ? columns.IndexOf(x => x.Style.ID) : -1; // int jobcol = hasjob ? columns.IndexOf(x => x.Job.ID) : -1; // int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize); // // var tuples = rows.Select(r => new Tuple( // (Guid)(r.Values[productcol] ?? Guid.Empty), // (hasstyle ? (Guid)(r.Values[stylecol] ?? Guid.Empty) : null), // (hasjob ? (Guid)(r.Values[jobcol] ?? Guid.Empty) : null), // (String)(r.Values[unitcol] ?? ""), // (double)(r.Values[aggcol] ?? 0.0d)) // ).ToArray(); // // var total = tuples.Aggregate(0d, (value, tuple) => value + tuple.Item5); if(aggregate is not null) { target?.Set(aggregate, total); } return total; } private class StockForecastJobInfo { public double BOM { get; set; } public double Stock { get; set; } public double PO { get; set; } public double Required => Math.Max(BOM - (Stock + PO), 0.0F); } private class StockForecastInfo { public double MinStock { get; set; } public double GenStock { get; set; } public double GenPO { get; set; } public double JobBOM { get; set; } public double JobStock { get; set; } public double JobPO { get; set; } public Dictionary JobInfo { get; private init; } = []; public void AddJobBOM(Guid jobID, double quantity) { var item = JobInfo.GetValueOrAdd(jobID); item.BOM += quantity; } public void AddJobPO(Guid jobID, double quantity) { var item = JobInfo.GetValueOrAdd(jobID); item.PO += quantity; } public void AddJobStock(Guid jobID, double quantity) { var item = JobInfo.GetValueOrAdd(jobID); item.Stock += quantity; } public double StockRequired => Math.Max(MinStock - (GenStock + GenPO), 0.0F); public double Required => Math.Max((MinStock + JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F); public double Optimised => Math.Max(Math.Max(MinStock, JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F); } private Dictionary _summaryinfo = new Dictionary(); private double? GetColumnCalculatedData(ColumnTag tag, Guid productInstanceID) { if (!_summaryinfo.TryGetValue(productInstanceID, out var info)) return null; return tag switch { ColumnTag.MinimumStockRequired => info.MinStock, ColumnTag.GeneralStockHoldings => info.GenStock, ColumnTag.GeneralPurchaseOrders => info.GenPO, ColumnTag.JobStockRequired => info.JobBOM, ColumnTag.JobStockHoldings => info.JobStock, ColumnTag.JobPurchaseOrders => info.JobPO, ColumnTag.BalanceRequired => (Optimise ? info.Optimised : info.Required), _ => null }; } private string[] GetColumnFilterItems(ColumnTag tag) { var selectedIDs = FilterRows(Data.Rows).Select(x => x.Get(x => x.ID)); var items = new HashSet(); foreach(var id in selectedIDs) { var value = GetColumnCalculatedData(tag, id); if (value.HasValue) { items.Add(value.Value.ToString("F2")); } } var arr = items.ToArray(); Array.Sort(arr); return arr; } protected override IEnumerable? GetColumnFilterItems(DynamicColumnBase column) { if (column.Tag is ColumnTag tag) { return GetColumnFilterItems(tag); } return base.GetColumnFilterItems(column); } protected override void Reload(Filters criteria, Columns columns, ref SortOrder? sort, Action action) { var query = new MultiQuery(); query.Add( GroupIDs.Length == 0 ? new Filter().None() : new Filter(pi=>pi.Product.Group.ID).InList(GroupIDs), columns, new SortOrder(x=>x.Product.Code) ); query.Add( GroupIDs.Length == 0 ? new Filter().None() : new Filter(x=>x.Product.Group.ID).InList(GroupIDs) .And(new Filter(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)), new Columns(x=>x.Product.ID) .Add(x=>x.Job.ID) .Add(x=>x.Style.ID) .Add(x=>x.Dimensions.Unit.ID) .Add(x=>x.Dimensions.UnitSize) .Add(x=>x.Units), null ); query.Add( GroupIDs.Length == 0 ? new Filter().None() : new Filter(x=>x.Product.Group.ID).InList(GroupIDs) .And(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue) .And(new Filter(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)), new Columns(x=>x.Product.ID) .Add(x=>x.Job.ID) .Add(x=>x.Style.ID) .Add(x=>x.Dimensions.Unit.ID) .Add(x=>x.Dimensions.UnitSize) .Add(x=>x.Qty), null ); query.Add( GroupIDs.Length == 0 ? new Filter().None() : new Filter(x=>x.Product.Group.ID).InList(GroupIDs) .And(new Filter(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)), new Columns(x=>x.Product.ID) .Add(x=>x.Job.ID) .Add(x=>x.Style.ID) .Add(x=>x.Dimensions.Unit.ID) .Add(x=>x.Dimensions.UnitSize) .Add(x=>x.Quantity), null ); query.Add( GroupIDs.Length == 0 ? new Filter().None() : new Filter(x=>x.Product.Group.ID).InList(GroupIDs) .And(x=>x.Type).IsEqualTo(StockMovementType.Issue) .And(new Filter(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)), new Columns(x=>x.Product.ID) .Add(x=>x.Job.ID) .Add(x=>x.Style.ID) .Add(x=>x.Dimensions.Unit.ID) .Add(x=>x.Dimensions.UnitSize) .Add(x=>x.Units), null ); query.Add( GroupIDs.Length == 0 ? new Filter().None() : new Filter(x => x.Product.Group.ID).InList(GroupIDs) .And(new Filter(x => x.Job.ID).InList(JobIDs).Or(x => x.Job.ID).IsEqualTo(Guid.Empty)), new Columns(x=>x.Product.ID) .Add(x=>x.Job.ID) .Add(x=>x.Style.ID) .Add(x=>x.Dimensions.Unit.ID) .Add(x=>x.Dimensions.UnitSize) .Add(x=>x.SupplierLink.ID) .Add(x=>x.CostPrice) .Add(x=>x.Discount) .Add(x=>x.TaxCode.ID), null ); query.Query(q => { _stockHoldings = query.Get(); _poItems = query.Get(); _jobBOMs = query.Get(); _stockMovements = query.Get(); _supplierProducts = query.Get(); var products = query.Get(); _summaryinfo.Clear(); var _productInstanceCols = new Columns(products.Columns); var _idCol = _productInstanceCols.IndexOf(x => x.ID); var _productIDCol = _productInstanceCols.IndexOf(x => x.Product.ID); var _styleIDCol = _productInstanceCols.IndexOf(x => x.Style.ID); var _unitSizeCol = _productInstanceCols.IndexOf(x => x.Dimensions.UnitSize); var _stockHoldingCols = new Columns(_stockHoldings.Columns); var _poItemCols = new Columns(_poItems.Columns); var _jobBOMColumns = new Columns(_jobBOMs.Columns); var _stockMovementCols = new Columns(_stockMovements.Columns); foreach (var row in products.Rows) { var _id = row.Values[_idCol] as Guid? ?? Guid.Empty; var _productid = row.Values[_productIDCol] as Guid? ?? Guid.Empty; var _styleid = row.Values[_styleIDCol] as Guid? ?? Guid.Empty; var _unitsize = row.Values[_unitSizeCol] as string ?? ""; var info = new StockForecastInfo(); info.MinStock = row.Get(x => x.MinimumStockLevel); var genstockrows = GetRows(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, [Guid.Empty]); info.GenStock = Aggregate(genstockrows, _stockHoldingCols, true, true, x=>x.Units); var genporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, [Guid.Empty]); info.GenPO = Aggregate(genporows, _poItemCols, true, true, x=>x.Qty); // Job BOMs { var bomrows = GetRows(_jobBOMs.Rows, _jobBOMColumns, _productid, _styleid, _unitsize, JobIDs); var bom = Aggregate(bomrows, _jobBOMColumns, true, true, x => x.Quantity); var mvmtrows = GetRows(_stockMovements.Rows, _stockMovementCols, _productid, _styleid, _unitsize, JobIDs); var mvmts = Aggregate(mvmtrows, _stockMovementCols, true, true, x => x.Units); info.JobBOM = bom - mvmts; var bomJobCol = _jobBOMColumns.IndexOf(x => x.Job.ID); var bomQtyCol = _jobBOMColumns.IndexOf(x => x.Quantity); foreach(var jobBOMRow in bomrows) { info.AddJobBOM(jobBOMRow.Get(bomJobCol), jobBOMRow.Get(bomQtyCol)); } var mvtJobCol = _stockMovementCols.IndexOf(x => x.Job.ID); var mvtQtyCol = _stockMovementCols.IndexOf(x => x.Units); foreach(var mvtRow in mvmtrows) { info.AddJobBOM(mvtRow.Get(mvtJobCol), -mvtRow.Get(mvtQtyCol)); } } // Job Stock { var jobstockrows = GetRows(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, JobIDs); info.JobStock = Aggregate(jobstockrows, _stockHoldingCols, true, true, x=>x.Units); var jobCol = _stockHoldingCols.IndexOf(x => x.Job.ID); var qtyCol = _stockHoldingCols.IndexOf(x => x.Units); foreach(var jobStockRow in jobstockrows) { info.AddJobStock(jobStockRow.Get(jobCol), jobStockRow.Get(qtyCol)); } } // Job PO { var jobporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, JobIDs); info.JobPO = Aggregate(jobporows, _poItemCols, true, true, x => x.Qty); var jobCol = _poItemCols.IndexOf(x => x.Job.ID); var qtyCol = _poItemCols.IndexOf(x => x.Qty); foreach(var jobPORow in jobporows) { info.AddJobPO(jobPORow.Get(jobCol), jobPORow.Get(qtyCol)); } } _summaryinfo[_id] = info; } // Process the tables here action.Invoke(products, null); }); } protected override bool FilterRecord(CoreRow row) { bool result = base.FilterRecord(row); if (_summaryinfo.TryGetValue(row.Get(x => x.ID), out StockForecastInfo? info)) { if (RequiredOnly) { result = result && Optimise ? !info.Optimised.IsEffectivelyEqual(0) : !info.Required.IsEffectivelyEqual(0); } if (!AllStock) result = result && _uicomponent?.CheckSuppliers(row) == true; } return result; } #endregion #region Ordering private IEnumerable FilterRows(IEnumerable rows) { var predicates = GetFilterPredicates(); return rows.Where(r => { return predicates.All(x => x.Item2(r)); }); } private bool SelectForOrder_Click(CoreRow? row) { if (row is null) { var menu = new ContextMenu(); menu.AddItem("Select all", null, () => { foreach (var row in FilterRows(Data.Rows)) { SelectedForOrder.Add(row.Get(x => x.ID)); InvalidateRow(row); } OrderButton.IsEnabled = SelectedForOrder.Count > 0; }); menu.AddItem("Deselect all", null, () => { SelectedForOrder.Clear(); InvalidateGrid(); OrderButton.IsEnabled = false; }); menu.IsOpen = true; return false; } else { var id = row.Get(x => x.ID); if (!SelectedForOrder.Remove(id)) { SelectedForOrder.Add(id); } OrderButton.IsEnabled = SelectedForOrder.Count > 0; InvalidateRow(row); return false; } } private BitmapImage? SelectForOrder_Image(CoreRow? row) { if(row is null) { return _cart; } else if(SelectedForOrder.Contains(row.Get(x => x.ID))) { return _cart; } else { return null; } } private bool OrderStock_Click(Button button, CoreRow[] rows) { rows = FilterRows(Data.Rows.Where(x => SelectedForOrder.Contains(x.Get(x => x.ID)))).ToArray(); if(rows.Length == 0) { return false; } var items = new List(); foreach(var instance in rows.ToObjects()) { var info = _summaryinfo.GetValueOrDefault(instance.ID); var item = new StockForecastOrderingItem(); item.Product.CopyFrom(instance.Product); item.Style.CopyFrom(instance.Style); item.Dimensions.CopyFrom(instance.Dimensions); item.RequiredQuantity = (Optimise ? info?.Optimised : info?.Required) ?? default; if(info is not null) { item.SetJobRequiredQuantity(Guid.Empty, info.StockRequired); foreach(var (id, jobInfo) in info.JobInfo) { item.SetJobRequiredQuantity(id, jobInfo.Required); } } else { item.SetJobRequiredQuantity(Guid.Empty, 0.0); } items.Add(item); } var window = new StockForecastOrderScreen(items); if(window.ShowDialog() != true) { return false; } var orders = new List>>(); foreach(var perSupplier in window.Results.GroupBy(x => x.Supplier.ID)) { var order = new PurchaseOrder(); order.Description = "Purchase Order created from Stock Forecast Screen"; order.RaisedBy.ID = App.EmployeeID; LookupFactory.DoLookup(order, x => x.SupplierLink, perSupplier.Key); var orderItems = new List(); var results = perSupplier.ToArray(); foreach(var item in results) { var orderItem = new PurchaseOrderItem(); orderItem.Product.ID = item.Item.Product.ID; orderItem.Style.ID = item.Item.Style.ID; orderItem.Job.ID = item.Job?.ID ?? Guid.Empty; orderItems.Add(orderItem); } LookupFactory.DoLookups( orderItems.Select(x => new Tuple(x, x.Product.ID)), x => x.Product); LookupFactory.DoLookups( orderItems.Select(x => new Tuple(x, x.Style.ID)), x => x.Style); LookupFactory.DoLookups( orderItems.Select(x => new Tuple(x, x.Job.ID)), x => x.Job); LookupFactory.DoLookups( orderItems.WithIndex().Select(x => new Tuple(x.Value, results[x.Key].SupplierProduct.TaxCode.ID)), x => x.TaxCode); foreach(var (i, item) in results.WithIndex()) { var orderItem = orderItems[i]; orderItem.Dimensions.CopyFrom(item.Item.Dimensions); orderItem.Qty = item.Quantity; orderItem.Cost = item.SupplierProduct.CostPrice; } orders.Add(new(order, orderItems)); } Client.Save(orders.Select(x => x.Item1), "Created from Stock Forecast screen"); foreach(var (order, orderItems) in orders) { foreach(var item in orderItems) { item.PurchaseOrderLink.ID = order.ID; } } Client.Save(orders.SelectMany(x => x.Item2), "Created from Stock Forecast screen"); SelectedForOrder.Clear(); OrderButton.IsEnabled = false; return true; } #endregion #region IDataModelSource public event DataModelUpdateEvent? OnUpdateDataModel; public string SectionName => "Stock Forecast"; public bool Optimise { get; set; } public bool AllStock { get; set; } public bool RequiredOnly { get; set; } public DataModel DataModel(Selection selection) { return new AutoDataModel(null); } #endregion }