123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071 |
- using Comal.Classes;
- using InABox.Clients;
- using InABox.Core;
- using InABox.DynamicGrid;
- using InABox.Wpf;
- using InABox.WPF;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Linq.Expressions;
- using System.Threading;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Media;
- using System.Windows.Media.Imaging;
- using PRSDimensionUtils;
- using StockMovement = Comal.Classes.StockMovement;
- namespace PRSDesktop;
- public class StockForecastJobInfo
- {
-
- public string JobNumber { get; set; }
-
- 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);
- }
-
- public class StockForecastItem : BaseObject
- {
- public ProductLink Product => InitializeField(ref _product, nameof(Product));
- private ProductLink? _product;
-
- public ProductStyleLink Style => InitializeField(ref _style, nameof(Style));
- private ProductStyleLink? _style;
- public StockDimensions Dimensions => InitializeField(ref _dimensions, nameof(Dimensions));
- private StockDimensions? _dimensions;
- public bool IsProductInstance { get; set; }
- 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<Guid, StockForecastJobInfo> 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);
- }
- public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataModelSource
- {
- private enum ColumnTag
- {
- MinimumStockRequired,
- GeneralStockHoldings,
- GeneralPurchaseOrders,
- JobStockRequired,
- JobStockHoldings,
- JobPurchaseOrders,
- BalanceRequired
- }
-
- private SupplierProduct[]? _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();
- private static readonly BitmapImage _product = PRSDesktop.Resources.product.AsBitmapImage();
- public Guid[] GroupIDs { get; set; } = [];
- public Guid[] JobIDs { get; set; } = [];
- public HashSet<Guid> SupplierIDs { get; set; } = [];
- private readonly Button OrderButton;
- private HashSet<CoreRow> SelectedForOrder = [];
- private DynamicGridCustomColumnsComponent<StockForecastItem> ColumnsComponent;
- private string ColumnsTag => "StockForecastGrid";
-
- public StockForecastGrid() : base()
- {
- DimensionUtils.ResetDimensionScriptCache();
-
- ColumnsComponent = new DynamicGridCustomColumnsComponent<StockForecastItem>(this, ColumnsTag);
-
- 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.Product.OrderStrategy);
- HiddenColumns.Add(x => x.Required);
- HiddenColumns.Add(x => x.Optimised);
- HiddenColumns.Add(x => x.MinStock);
- HiddenColumns.Add(x => x.GenStock);
- HiddenColumns.Add(x => x.GenPO);
- HiddenColumns.Add(x => x.JobBOM);
- HiddenColumns.Add(x => x.JobStock);
- HiddenColumns.Add(x => x.JobPO);
-
- ActionColumns.Add(new DynamicImageColumn(ProductInstance_Image, null)
- {
- Position = DynamicActionColumnPosition.Start,
- ToolTip = ProductInstance_ToolTip
- });
- ActionColumns.Add(new DynamicImageColumn(Issues_Image, null)
- {
- ToolTip = Issues_Tooltip,
- Position = DynamicActionColumnPosition.Start
- });
- ActionColumns.Add(new DynamicImagePreviewColumn<StockForecastItem>(x => x.Product.Image)
- {
- Position = DynamicActionColumnPosition.Start
- });
-
- CreateColumn(GetMinimumStockLevel, ColumnTag.MinimumStockRequired,"Min.","F2");
- CreateColumn(GetGeneralStockLevel, ColumnTag.GeneralStockHoldings,"Hld.","F2");
- CreateColumn(GetGeneralPurchaseOrder, ColumnTag.GeneralPurchaseOrders, "PO.","F2");
- CreateColumn(GetBOMBalance, ColumnTag.JobStockRequired, "BOM.","F2");
- CreateColumn(GetReservedStock, ColumnTag.JobStockHoldings, "Hld.","F2");
- CreateColumn(GetReservedPurchaseOrder, ColumnTag.JobPurchaseOrders, "PO.","F2");
- CreateColumn(GetBalanceRequired, ColumnTag.BalanceRequired,"Req.","");
- ActionColumns.Add(new DynamicImageColumn(SelectForOrder_Image, SelectForOrder_Click)
- {
- Position = DynamicActionColumnPosition.End
- });
-
- OrderButton = AddButton("Order Stock", _cart, OrderStock_Click);
- OrderButton.IsEnabled = false;
- }
- private FrameworkElement? ProductInstance_ToolTip(DynamicActionColumn column, CoreRow? row)
- {
- if(row is null)
- {
- return column.TextToolTip("Does each line match a product instance?");
- }
- else if (LoadItem(row).IsProductInstance)
- {
- return column.TextToolTip("This line matches a product instance.");
- }
- else
- {
- return column.TextToolTip("This line does not match a product instance.");
- }
- }
- private BitmapImage? ProductInstance_Image(CoreRow? row)
- {
- if(row is null || LoadItem(row).IsProductInstance)
- {
- return _product;
- }
- else
- {
- return null;
- }
- }
- #region Columns
- protected override DynamicGridColumns LoadColumns()
- {
- return ColumnsComponent.LoadColumns();
- }
- protected override void LoadColumnsMenu(ContextMenu menu)
- {
- ColumnsComponent.LoadColumnsMenu(menu);
- }
- protected override void SaveColumns(DynamicGridColumns columns)
- {
- ColumnsComponent.SaveColumns(columns);
- }
- #endregion
- private BitmapImage? Issues_Image(CoreRow? row)
- {
- return (row is null)
- ? _warning
- : row.Get<StockForecastItem, string>(x => x.Product.Issues).IsNullOrWhiteSpace()
- ? null
- : _warning;
- }
- private FrameworkElement? Issues_Tooltip(DynamicActionColumn column, CoreRow? row)
- {
- return (row is null)
- ? null
- : column.TextToolTip(row.Get<StockForecastItem, string>(x => x.Product.Issues));
- }
- #region UIComponent
- private UIComponent? _uicomponent = null;
- private class UIComponent : DynamicGridGridUIComponent<StockForecastItem>
- {
- private StockForecastGrid Grid;
- public UIComponent(StockForecastGrid grid)
- {
- Grid = grid;
- Parent = grid;
- }
- public bool CheckSuppliers(CoreRow row)
- {
- if (Grid._supplierProducts == null)
- return false;
- var item = row.ToObject<StockForecastItem>(); //Grid.LoadItem(row));
- return Grid._supplierProducts.Any(r =>
- Equals(r.Product.ID, item.Product.ID)
- && Equals(r.Style.ID, item.Style.ID)
- //&& r.Dimensions.Unit.ID.Equals(item.Dimensions.Unit.ID)
- && Grid.SupplierIDs.Contains(r.SupplierLink.ID));
- }
-
- protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
- {
- if (column is DynamicTextColumn col)
- {
- var item = Grid.LoadItem(row);
- var stock = Math.Max(0.0F, item.MinStock - (item.GenStock + item.GenPO)).IsEffectivelyEqual(0.0F)
- ? Colors.LightBlue.ToBrush(0.5)
- : Colors.LightSalmon.ToBrush(0.5);
- var job = Math.Max(0.0F, item.JobBOM - (item.JobStock + item.JobPO)).IsEffectivelyEqual(0.0F)
- ? Colors.LightGreen.ToBrush(0.5)
- : Colors.LightSalmon.ToBrush(0.5);
-
- var overall = !(Grid.Optimise ? item.Optimised : item.Required).IsEffectivelyEqual(0.0F)
- ? Colors.LightSalmon.ToBrush(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 Colors.Silver.ToBrush(0.5);
- }
-
- return null;
- }
- }
- protected override IDynamicGridUIComponent<StockForecastItem> CreateUIComponent()
- {
- return _uicomponent ??= new UIComponent(this);
- }
- #endregion
- protected override void DoReconfigure(DynamicGridOptions options)
- {
- base.DoReconfigure(options);
- options.Clear();
- options.RecordCount = true;
- options.SelectColumns = true;
- options.FilterRows = true;
- options.ExportData = true;
- options.MultiSelect = true;
- options.HideDatabaseFilters = true;
- }
- 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<StockForecastItem>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
- columns.Add<StockForecastItem>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
- columns.Add<StockForecastItem>(x => x.Style.Code, 120, "Style Code", "", Alignment.MiddleCenter);
- columns.Add<StockForecastItem>(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,
- GetFilter = () => new DynamicColumnFilter<double?>(
- r => GetColumnCalculatedData(tag, r),
- () => GetColumnFilterItems(tag))
- };
- ActionColumns.Add(column);
- }
- private DynamicTextColumn GetColumn(ColumnTag tag) => (ActionColumns.First(x => Equals(x.Tag, tag)) as DynamicTextColumn)!;
- private object GetMinimumStockLevel(CoreRow? row) => row is not null ? LoadItem(row).MinStock : 0.0;
- private object GetGeneralStockLevel(CoreRow? row)
- => row is not null ? LoadItem(row).GenStock : 0.0;
- private object GetGeneralPurchaseOrder(CoreRow? row)
- => row is not null ? LoadItem(row).GenPO : 0.0;
-
- private object GetBOMBalance(CoreRow? row)
- => row is not null ? LoadItem(row).JobBOM : 0.0;
- private object GetReservedStock(CoreRow? row)
- => row is not null ? LoadItem(row).JobStock : 0.0;
- private object GetReservedPurchaseOrder(CoreRow? row)
- => row is not null ? LoadItem(row).JobPO : 0.0;
- private object GetBalanceRequired(CoreRow? row)
- {
- if(row is not null)
- {
- var item = LoadItem(row);
- return Optimise
- ? (item.Optimised.IsEffectivelyEqual(0.0) ? "" : $"{item.Optimised:F2}")
- : (item.Required.IsEffectivelyEqual(0.0) ? "" : $"{item.Required:F2}");
- }
- else
- {
- return "";
- }
- }
-
- private void ShowDetailGrid(String title, params Func<IDynamicDataGrid?>[] gridfuncs)
- {
- var _window = new ThemableWindow { Title = title };
- var _tabcontrol = new DynamicTabControl() { TabStripPlacement = Dock.Bottom, Margin = new Thickness(5) };
- _window.Content = _tabcontrol;
- foreach (var gridfunc in gridfuncs)
- {
- var _grid = gridfunc();
- if (_grid != null)
- {
- _tabcontrol.Items.Add(
- new DynamicTabItem()
- {
- Header = CoreUtils.Neatify(_grid.DataType.Name.Split('.').Last()),
- Content = _grid
- }
- );
- _grid.Refresh(true,true);
- }
- }
- _window.ShowDialog();
- }
-
- private IDynamicDataGrid BuildDetailGrid<TEntity>(
- String tag,
- Expression<Func<TEntity,object?>> productcol,
- Guid productid,
- Expression<Func<TEntity,object?>> stylecol,
- Guid? styleid,
- Expression<Func<TEntity, IDimensions>> dimcol,
- IDimensions? dimensions,
- Expression<Func<TEntity,object?>>? jobcol,
- Filter<TEntity>? extrafilter,
- Func<CoreRow,bool>? 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 null;
- }
- _grid.ColumnsTag = $"{ColumnsTag}.{tag}";
- _grid.Reconfigure(options =>
- {
- options.Clear();
- options.FilterRows = true;
- options.SelectColumns = true;
- });
- _grid.OnDefineFilter += t =>
- {
- var _filter = new Filter<TEntity>(productcol).IsEqualTo(productid);
- if(dimensions is not null)
- _filter = _filter.And(CoreUtils.GetFullPropertyName(dimcol, ".")).DimensionEquals(dimensions);
-
- if (styleid.HasValue)
- _filter = _filter.And(stylecol).IsEqualTo(styleid);
- if (jobcol != null)
- _filter = _filter.And(new Filter<TEntity>(jobcol).InList(JobIDs));
-
- if (extrafilter != null)
- _filter = _filter.And(extrafilter);
-
- return _filter;
- };
- _grid.OnFilterRecord += row => rowfilter?.Invoke(row) ?? true;
- return _grid;
- }
- protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
- {
- //base.DoDoubleClick(sender, args);
- if (args.Row is null || args.Column?.Tag is not ColumnTag tag) return;
- var item = LoadItem(args.Row);
- var styleid = HasStyle() ? item.Style.ID : (Guid?)null;
- switch (tag)
- {
- case ColumnTag.GeneralStockHoldings:
- ShowDetailGrid(
- "Stock Holdings",
- () => BuildDetailGrid<StockHolding>(
- ColumnTag.GeneralStockHoldings.ToString(),
- x => x.Product.ID,
- item.Product.ID,
- x => x.Style.ID,
- styleid,
- x => x.Dimensions,
- item.Dimensions,
- null,
- new Filter<StockHolding>(x=>x.Job.ID).IsEqualTo(Guid.Empty),
- null
- )
- );
- break;
- case ColumnTag.GeneralPurchaseOrders:
- //ShowDetailGrid(
- // "Purchase Order Allocations",
- // () => BuildDetailGrid<PurchaseOrderItemAllocation>(
- // ColumnTag.GeneralPurchaseOrders.ToString(),
- // x => x.Item.Product.ID,
- // item.Product.ID,
- // x => x.Item.Style.ID,
- // styleid,
- // x => x.Item.Dimensions,
- // item.Dimensions,
- // null,
- // new Filter<PurchaseOrderItemAllocation>(x=>x.Job.ID).IsEqualTo(Guid.Empty)
- // .And(x=>x.Item.ReceivedDate).IsEqualTo(DateTime.MinValue),
- // null
- // )
- //);
- break;
- case ColumnTag.JobStockRequired:
- ShowDetailGrid(
- "Bills Of Materials",
- () => BuildDetailGrid<JobBillOfMaterialsItem>(
- ColumnTag.JobStockRequired.ToString(),
- x => x.Product.ID,
- item.Product.ID,
- x => x.Style.ID,
- styleid,
- x => x.Dimensions,
- item.Dimensions,
- x => x.Job.ID,
- new Filter<JobBillOfMaterialsItem>(x=>x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue),
- null
- ),
- () => BuildDetailGrid<StockMovement>(
- "JobStockIssued",
- x => x.Product.ID,
- item.Product.ID,
- x => x.Style.ID,
- styleid,
- x => x.Dimensions,
- item.Dimensions,
- x => x.Job.ID,
- new Filter<StockMovement>(x=>x.Type).IsEqualTo(StockMovementType.Issue),
- null
- )
-
- );
- break;
- case ColumnTag.JobStockHoldings:
- ShowDetailGrid(
- "Stock Holdings",
- () => BuildDetailGrid<StockHolding>(
- ColumnTag.JobStockHoldings.ToString(),
- x => x.Product.ID,
- item.Product.ID,
- x => x.Style.ID,
- styleid,
- x => x.Dimensions,
- item.Dimensions,
- x => x.Job.ID,
- null,
- null
- )
- );
- break;
- case ColumnTag.JobPurchaseOrders:
- //ShowDetailGrid(
- // "Purchase Orders",
- // () => BuildDetailGrid<PurchaseOrderItemAllocation>(
- // ColumnTag.GeneralPurchaseOrders.ToString(),
- // x => x.Item.Product.ID,
- // item.Product.ID,
- // x => x.Item.Style.ID,
- // styleid,
- // x => x.Item.Dimensions,
- // item.Dimensions,
- // x => x.Job.ID,
- // new Filter<PurchaseOrderItemAllocation>(x => x.Item.ReceivedDate).IsEqualTo(DateTime.MinValue),
- // null
- // )
- //);
- break;
- }
- }
- #endregion
- #region Refresh
- private bool HasStyle()
- {
- return DataColumns().ColumnNames().Any(x => x.StartsWith("Style.") && !x.Equals("Style.ID"));
- }
- private CoreRow[] GetRows<TSource>(CoreTable table, Guid productid, Guid? styleid, IDimensions dimensions, Guid[] jobids) where TSource : IJobMaterial
- {
- int productcol = table.GetColumnIndex<TSource>(x => x.Product.ID);
- int stylecol = styleid.HasValue ? table.GetColumnIndex<TSource>(x => x.Style.ID) : -1;
- var dimCols = Dimensions.GetFilterColumnIndices<TSource>(table, x => x.Dimensions);
- int jobcol = table.GetColumnIndex<TSource>(x => x.Job.ID);
-
- var subset = table.Rows
- .Where(r =>
- Guid.Equals(r.Values[productcol], productid)
- && (!styleid.HasValue || Guid.Equals(r.Values[stylecol], styleid))
- && r.ToDimensions<StockDimensions>(dimCols).Equals(dimensions)
- && jobids.Any(x=>Equals(x,r.Values[jobcol]))
- );
-
- return subset.ToArray();
- }
- private double Aggregate<TSource>(CoreTable table, IEnumerable<CoreRow> rows, bool hasstyle, bool hasjob, Expression<Func<TSource, object>> source, CoreRow? target = null, Expression<Func<ProductInstance, object>>? aggregate = null)
- {
- int srcol = table.GetColumnIndex(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, Guid?, Guid?, String, double>(
- // (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 double? GetColumnCalculatedData(ColumnTag tag, CoreRow row)
- {
- return tag switch
- {
- ColumnTag.MinimumStockRequired => row.Get<StockForecastItem, double>(x => x.MinStock),
- ColumnTag.GeneralStockHoldings => row.Get<StockForecastItem, double>(x => x.GenStock),
- ColumnTag.GeneralPurchaseOrders => row.Get<StockForecastItem, double>(x => x.GenPO),
- ColumnTag.JobStockRequired => row.Get<StockForecastItem, double>(x => x.JobBOM),
- ColumnTag.JobStockHoldings => row.Get<StockForecastItem, double>(x => x.JobStock),
- ColumnTag.JobPurchaseOrders => row.Get<StockForecastItem, double>(x => x.JobPO),
- ColumnTag.BalanceRequired => (Optimise ? row.Get<StockForecastItem, double>(x => x.Optimised) : row.Get<StockForecastItem, double>(x => x.Required)),
- _ => null
- };
- }
- private IEnumerable<Tuple<string, double?>> GetColumnFilterItems(ColumnTag tag)
- {
- var items = new HashSet<double>();
- foreach(var row in Data.Rows)
- {
- var value = GetColumnCalculatedData(tag, row);
- if (value.HasValue)
- {
- items.Add(value.Value);
- }
- }
- var arr = items.ToArray();
- Array.Sort(arr);
- return arr.Select(x => new Tuple<string, double?>(x.ToString("F2"), x));
- }
- private class ItemKey(Guid productID, Guid styleID, StockDimensions dimensions)
- {
- public Guid ProductID { get; set; } = productID;
- public Guid StyleID { get; set; } = styleID;
- public StockDimensions Dimensions { get; set; } = dimensions;
- public override bool Equals(object? obj)
- {
- return obj is ItemKey other
- && ProductID == other.ProductID
- && StyleID == other.StyleID
- && Dimensions.Equals(other.Dimensions);
- }
- public override int GetHashCode()
- {
- return HashCode.Combine(ProductID, StyleID, Dimensions);
- }
- }
- protected override void Reload(Filters<StockForecastItem> criteria, Columns<StockForecastItem> columns, ref SortOrder<StockForecastItem>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
- {
- // Need to query ProductInstances, StockHoldings, Job BOM and PO.
- KeyedQueryDef<T> GetQuery<T>(Filter<T>? filter = null, Columns<T>? columns = null) where T : Entity, IJobMaterial, IRemotable, IPersistent, new()
- {
- return new KeyedQueryDef<T>(
- Filter<T>.And(
- new Filter<T>(x => x.Product.Group.ID).InList(GroupIDs)
- .And(new Filter<T>(x => x.Job.ID).InList(JobIDs)
- .Or(x => x.Job.ID).IsEqualTo(Guid.Empty)),
- filter),
- Columns.None<T>()
- .Add(x => x.Product.ID)
- .Add(x => x.Job.ID)
- .Add(x => x.Style.ID)
- .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data)
- .Add(x => x.Dimensions.UnitSize)
- .Add(x => x.Dimensions.Value)
- .AddRange(columns ?? Enumerable.Empty<Column<T>>()));
- }
- ItemKey GetKeyFromRow(CoreRow row)
- {
- var key = new ItemKey(
- row.Get<IJobMaterial, Guid>(x => x.Product.ID),
- row.Get<IJobMaterial, Guid>(x => x.Style.ID),
- row.ToDimensions<IJobMaterial, StockDimensions>(x => x.Dimensions));
- key.Dimensions.UnitSize = row.Get<IJobMaterial, string>(x => x.Dimensions.UnitSize);
- key.Dimensions.Value = row.Get<IJobMaterial, double>(x => x.Dimensions.Value);
- return key;
- }
- ItemKey GetKey(Guid productid, Guid styleid, StockDimensions dimensions)
- {
- var key = new ItemKey(
- productid,
- styleid,
- dimensions);
- return key;
- }
- var queries = new IKeyedQueryDef[]
- {
- new KeyedQueryDef<ProductInstance>(
- new Filter<ProductInstance>(x=>x.MinimumStockLevel).IsNotEqualTo(0.0).And(x => x.Product.Group.ID).InList(GroupIDs),
- Columns.None<ProductInstance>()
- .Add(x => x.Product.ID)
- .Add(x => x.Style.ID)
- .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data)
- .Add(x => x.Dimensions.UnitSize)
- .Add(x => x.Dimensions.Value)
- .Add(x => x.MinimumStockLevel)),
- GetQuery<StockHolding>(
- columns: Columns.None<StockHolding>().Add(x => x.Units)),
- new KeyedQueryDef<PurchaseOrderItem>(
- new Filter<PurchaseOrderItem>(x => x.ReceivedDate).IsEqualTo(DateTime.MinValue)
- .And(x => x.Product.Group.ID).InList(GroupIDs),
- Columns.None<PurchaseOrderItem>()
- .Add(x => x.ID)
- .Add(x => x.Qty)
- .Add(x => x.Product.ID)
- .Add(x => x.Style.ID)
- .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data)
- .Add(x => x.Dimensions.UnitSize)
- .Add(x => x.Dimensions.Value)),
- new KeyedQueryDef<PurchaseOrderItemAllocation>(
- new Filter<PurchaseOrderItemAllocation>(x => x.Item.ReceivedDate).IsEqualTo(DateTime.MinValue)
- .And(x => x.Item.Product.Group.ID).InList(GroupIDs)
- .And(new Filter<PurchaseOrderItemAllocation>(x => x.Job.ID).InList(JobIDs)
- .Or(x => x.Job.ID).IsEqualTo(Guid.Empty)),
- Columns.None<PurchaseOrderItemAllocation>()
- .Add(x => x.Quantity)
- .Add(x => x.Job.ID)
- .Add(x => x.Item.ID)
- .Add(x => x.Item.Product.ID)
- .Add(x => x.Item.Style.ID)
- .AddDimensionsColumns(x => x.Item.Dimensions, Dimensions.ColumnsType.Data)
- .Add(x => x.Item.Dimensions.UnitSize)
- .Add(x => x.Item.Dimensions.Value)),
- GetQuery<JobBillOfMaterialsItem>(
- filter: new Filter<JobBillOfMaterialsItem>(x=>x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue),
- columns: Columns.None<JobBillOfMaterialsItem>().Add(x => x.Quantity)),
- GetQuery<StockMovement>(
- filter: new Filter<StockMovement>(x => x.Type).IsEqualTo(StockMovementType.Issue),
- columns: Columns.None<StockMovement>().Add(x => x.Units)),
- GetQuery<SupplierProduct>(
- columns: Columns.None<SupplierProduct>()
- .Add(x => x.SupplierLink.ID)
- .Add(x => x.CostPrice)
- .Add(x => x.Discount)
- .Add(x => x.TaxCode.ID))
- };
- Client.QueryMultiple(
- (QueryMultipleResults? results, Exception? error) =>
- {
- if (results is null)
- {
- action(null, error);
- return;
- }
- var items = new Dictionary<ItemKey, StockForecastItem>();
- StockForecastItem GetItem(ItemKey key)
- {
- if(!items.TryGetValue(key, out var item))
- {
- item = new StockForecastItem();
- item.Product.ID = key.ProductID;
- item.Style.ID = key.StyleID;
- item.Dimensions.CopyFrom(key.Dimensions);
- items[key] = item;
- }
- return item;
- }
- var productInstances = results.GetArray<ProductInstance>();
- foreach(var instance in productInstances)
- {
- var item = GetItem(new(instance.Product.ID, instance.Style.ID, instance.Dimensions));
- item.MinStock += DimensionUtils.ConvertDimensions(instance.Dimensions, instance.MinimumStockLevel, (f,c) => Client.Query(f,c));
- item.IsProductInstance = true;
- }
- var holdings = results.Get<StockHolding>();
- foreach(var holdingrow in holdings.Rows)
- {
- var holding = holdingrow.ToObject<StockHolding>();
- holding.Units = DimensionUtils.ConvertDimensions(holding.Dimensions, holding.Units, (f,c) => Client.Query(f,c));
- var item = GetItem(GetKey(holding.Product.ID, holding.Style.ID, holding.Dimensions));
- if(holding.Job.ID == Guid.Empty)
- {
- item.GenStock += holding.Units;
- }
- else
- {
- item.JobStock += holding.Units;
- item.AddJobStock(holding.Job.ID, holding.Units);
- }
- }
- var purchaseOrderItems = results.GetObjects<PurchaseOrderItem>()
- .ToDictionary(x => x.ID);
- foreach (var poi in purchaseOrderItems.Values)
- {
- poi.Qty = DimensionUtils.ConvertDimensions(poi.Dimensions, poi.Qty, (f,c) => Client.Query(f,c));
- }
-
- var allocations = results.Get<PurchaseOrderItemAllocation>();
- foreach(var allocationrow in allocations.Rows)
- {
- var allocation = allocationrow.ToObject<PurchaseOrderItemAllocation>();
- var q = allocation.Quantity;
- DimensionUtils.ConvertDimensions(allocation.Item.Dimensions, ref q, (f,c) => Client.Query(f,c));
- // POIAs are already converted where necessary, so we don't have to update the quantities again, just update the dimensions
-
- var key = new ItemKey(
- allocation.Item.Product.ID,
- allocation.Item.Style.ID,
- allocation.Item.Dimensions);
- var item = GetItem(key);
- if(allocation.Job.ID == Guid.Empty)
- {
- item.GenPO += allocation.Quantity;
- }
- else
- {
- item.JobPO += allocation.Quantity;
- item.AddJobPO(allocation.Job.ID, allocation.Quantity);
- }
- if(purchaseOrderItems.TryGetValue(allocation.Item.ID, out var poi))
- poi.Qty -= allocation.Quantity;
- }
- foreach(var poi in purchaseOrderItems.Values)
- {
- var key = new ItemKey(
- poi.Product.ID,
- poi.Style.ID,
- poi.Dimensions);
- var item = GetItem(key);
- if(poi.Job.ID == Guid.Empty)
- {
- item.GenPO += poi.Qty;
- }
- else
- {
- item.JobPO += poi.Qty;
- item.AddJobPO(poi.Job.ID, poi.Qty);
- }
- }
- var jobBOMItems = results.Get<JobBillOfMaterialsItem>();
- foreach(var bomItemRow in jobBOMItems.Rows)
- {
- var bomItem = bomItemRow.ToObject<JobBillOfMaterialsItem>();
- var quantity = bomItem.Quantity;
- DimensionUtils.ConvertDimensions(bomItem.Dimensions, ref quantity, (f,c) => Client.Query(f,c));
- bomItem.Quantity = quantity;
- var key = GetKey(bomItem.Product.ID, bomItem.Style.ID, bomItem.Dimensions);
- var item = GetItem(key);
- item.AddJobBOM(bomItem.Job.ID, bomItem.Quantity);
- }
- var movements = results.Get<StockMovement>();
- foreach(var mvtrow in movements.Rows)
- {
- var movement = mvtrow.ToObject<StockMovement>();
- var units = movement.Units;
- DimensionUtils.ConvertDimensions(movement.Dimensions, ref units, (f,c) => Client.Query(f,c));
- movement.Units = units;
-
- var item = GetItem(GetKey(movement.Product.ID, movement.Style.ID, movement.Dimensions));
-
- if(movement.Job.ID != Guid.Empty)
- item.AddJobBOM(movement.Job.ID, movement.Units);
- }
- _supplierProducts = results.GetArray<SupplierProduct>();
- Items = items.Values.ToList();
- foreach(var item in Items)
- {
- foreach(var (job, info) in item.JobInfo)
- {
- info.BOM = Math.Max(info.BOM, 0.0);
- }
- item.JobBOM = item.JobInfo.Sum(x => x.Value.BOM);
- }
- Items.LoadForeignProperties(columns);
- Items.Sort((a, b) => a.Product.Code.CompareTo(b.Product.Code));
- var result = new CoreTable();
- result.LoadColumns(columns);
- result.LoadRows(Items);
- action(result, null);
- }, queries);
- }
- protected override bool FilterRecord(CoreRow row)
- {
- bool result = base.FilterRecord(row);
- if (RequiredOnly)
- {
- result = result && Optimise
- ? !row.Get<StockForecastItem, double>(x => x.Optimised).IsEffectivelyEqual(0)
- : !row.Get<StockForecastItem, double>(x => x.Required).IsEffectivelyEqual(0);
- }
- if (!AllStock)
- result = result && _uicomponent?.CheckSuppliers(row) == true;
- return result;
- }
- #endregion
- #region Ordering
- private IEnumerable<CoreRow> FilterRows(IEnumerable<CoreRow> 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);
- InvalidateRow(row);
- }
- OrderButton.IsEnabled = SelectedForOrder.Count > 0;
- });
- menu.AddItem("Deselect all", null, () =>
- {
- SelectedForOrder.Clear();
- InvalidateGrid();
- OrderButton.IsEnabled = false;
- });
- menu.IsOpen = true;
- return false;
- }
- else
- {
- if (!SelectedForOrder.Remove(row))
- {
- SelectedForOrder.Add(row);
- }
- 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))
- {
- return _cart;
- }
- else
- {
- return null;
- }
- }
-
- private bool OrderStock_Click(Button button, CoreRow[] rows)
- {
- rows = FilterRows(Data.Rows.Where(x => SelectedForOrder.Contains(x))).ToArray();
- if(rows.Length == 0)
- {
- return false;
- }
- var items = new List<StockForecastOrderData>();
- foreach(var forecastItem in LoadItems(rows))
- {
- var item = new StockForecastOrderData(forecastItem.Product, forecastItem.Style, forecastItem.Dimensions);
- item.RequiredQuantity = Optimise ? forecastItem.Optimised : forecastItem.Required;
- if(forecastItem.StockRequired > 0)
- {
- item.SetRequiredQuantity(Guid.Empty, Guid.Empty, "", forecastItem.StockRequired);
- }
-
- foreach(var (id, jobInfo) in forecastItem.JobInfo)
- {
- if (jobInfo.Required > 0)
- item.SetRequiredQuantity(id, Guid.Empty, jobInfo.JobNumber, jobInfo.Required);
- }
- items.Add(item);
- }
- var window = new StockForecastOrderScreen(items) { Owner = App.Current.MainWindow };
- if(window.ShowDialog() != true)
- {
- return false;
- }
- window.CreateOrders("Stock Forecast");
-
- 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<ProductInstance>(null);
- }
-
- #endregion
- }
|