1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000 |
- using Comal.Classes;
- using InABox.Clients;
- using InABox.Core;
- using InABox.DynamicGrid;
- using InABox.Wpf;
- using InABox.WPF;
- using jdk.nashorn.@internal.ir;
- using PRSDesktop.Panels.StockForecast.OrderScreen;
- 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;
- namespace PRSDesktop;
- public 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);
- }
-
- public class StockForecastItem : BaseObject
- {
- public ProductLink Product { get; set; }
-
- public ProductStyleLink Style { get; set; }
- public StockDimensions Dimensions { get; set; }
- 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()
- {
- 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.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 = Grid.LoadItem(row);
- return Grid._supplierProducts.Any(r =>
- Equals(r.Product.ID, item.Product.ID)
- && Equals(r.Style.ID, item.Style.ID)
- && r.Dimensions.Equals(item.Dimensions)
- && 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, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
- columns.Add<StockForecastItem, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
- columns.Add<StockForecastItem, string>(x => x.Style.Code, 120, "Style Code", "", Alignment.MiddleCenter);
- columns.Add<StockForecastItem, string>(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,
- FilterRecord = (row, filters) =>
- {
- if (filters.Length == 1 && filters[0].Length == 0) return true;
- var value = GetColumnCalculatedData(tag, row);
- 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 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<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;
- }
- 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;
- var window = DynamicGridUtils.CreateGridWindow($"Viewing {CoreUtils.Neatify(tag)} Calculation", grid);
- window.ShowDialog();
- }
- 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<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<PurchaseOrderItem>(
- ColumnTag.GeneralPurchaseOrders.ToString(),
- x => x.Product.ID,
- item.Product.ID,
- x => x.Style.ID,
- styleid,
- x => x.Dimensions,
- item.Dimensions,
- null,
- new Filter<PurchaseOrderItem>(x=>x.Job.ID).IsEqualTo(Guid.Empty)
- .And(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue),
- null);
- break;
- case ColumnTag.JobStockRequired:
- ShowDetailGrid<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);
- break;
- case ColumnTag.JobStockHoldings:
- ShowDetailGrid<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<PurchaseOrderItem>(
- ColumnTag.GeneralPurchaseOrders.ToString(),
- x => x.Product.ID,
- item.Product.ID,
- x => x.Style.ID,
- styleid,
- x => x.Dimensions,
- item.Dimensions,
- x => x.Job.ID,
- new Filter<PurchaseOrderItem>(x=>x.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 string[] GetColumnFilterItems(ColumnTag tag)
- {
- var items = new HashSet<string>();
- foreach(var row in Data.Rows)
- {
- var value = GetColumnCalculatedData(tag, row);
- if (value.HasValue)
- {
- items.Add(value.Value.ToString("F2"));
- }
- }
- var arr = items.ToArray();
- Array.Sort(arr);
- return arr;
- }
- protected override IEnumerable<string>? GetColumnFilterItems(DynamicColumnBase column)
- {
- if (column.Tag is ColumnTag tag)
- {
- return GetColumnFilterItems(tag);
- }
- return base.GetColumnFilterItems(column);
- }
- 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)
- {
- Items.Clear();
- // 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.Local)
- .Add(x => x.Dimensions.UnitSize)
- .Add(x => x.Dimensions.Value)
- .AddRange(columns ?? Enumerable.Empty<Column<T>>()));
- }
- ItemKey GetKey(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;
- }
- var queries = new IKeyedQueryDef[]
- {
- new KeyedQueryDef<ProductInstance>(
- new Filter<ProductInstance>(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.Local)
- .Add(x => x.Dimensions.UnitSize)
- .Add(x => x.Dimensions.Value)
- .Add(x => x.MinimumStockLevel)),
- GetQuery<StockHolding>(
- columns: Columns.None<StockHolding>().Add(x => x.Units)),
- GetQuery<PurchaseOrderItem>(
- filter: new Filter<PurchaseOrderItem>(x => x.ReceivedDate).IsEqualTo(DateTime.MinValue),
- columns: Columns.None<PurchaseOrderItem>().Add(x => x.Qty)),
- GetQuery<JobBillOfMaterialsItem>(
- 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 += instance.MinimumStockLevel;
- item.IsProductInstance = true;
- }
- var holdings = results.Get<StockHolding>();
- foreach(var holding in holdings.Rows)
- {
- var item = GetItem(GetKey(holding));
- var jobID = holding.Get<StockHolding, Guid>(x => x.Job.ID);
- if(jobID == Guid.Empty)
- {
- item.GenStock += holding.Get<StockHolding, double>(x => x.Units);
- }
- else
- {
- item.JobStock += holding.Get<StockHolding, double>(x => x.Units);
- item.AddJobStock(jobID, holding.Get<StockHolding, double>(x => x.Units));
- }
- }
- var poItems = results.Get<PurchaseOrderItem>();
- foreach(var poItem in poItems.Rows)
- {
- var item = GetItem(GetKey(poItem));
- var jobID = poItem.Get<PurchaseOrderItem, Guid>(x => x.Job.ID);
- if(jobID == Guid.Empty)
- {
- item.GenPO += poItem.Get<PurchaseOrderItem, double>(x => x.Qty);
- }
- else
- {
- item.JobPO += poItem.Get<PurchaseOrderItem, double>(x => x.Qty);
- item.AddJobPO(jobID, poItem.Get<PurchaseOrderItem, double>(x => x.Qty));
- }
- }
- var jobBOMItems = results.Get<JobBillOfMaterialsItem>();
- foreach(var bomItem in jobBOMItems.Rows)
- {
- var item = GetItem(GetKey(bomItem));
- item.JobBOM += bomItem.Get<JobBillOfMaterialsItem, double>(x => x.Quantity);
- item.AddJobBOM(bomItem.Get<JobBillOfMaterialsItem, Guid>(x => x.Job.ID), bomItem.Get<JobBillOfMaterialsItem, double>(x => x.Quantity));
- }
- var movements = results.Get<StockMovement>();
- foreach(var mvt in movements.Rows)
- {
- var item = GetItem(GetKey(mvt));
- item.JobBOM -= mvt.Get<StockMovement, double>(x => x.Units);
- item.AddJobBOM(mvt.Get<StockMovement, Guid>(x => x.Job.ID), -mvt.Get<StockMovement, double>(x => x.Units));
- }
- _supplierProducts = results.GetArray<SupplierProduct>();
- Items = items.Values.ToList();
- Items.LoadForeignProperties(columns);
- Items.Sort((a, b) => a.Product.Code.CompareTo(b.Product.Code));
- //Items.Sort(x => x.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;
- item.SetJobRequiredQuantity(Guid.Empty, forecastItem.StockRequired);
- foreach(var (id, jobInfo) in forecastItem.JobInfo)
- {
- item.SetJobRequiredQuantity(id, jobInfo.Required);
- }
- items.Add(item);
- }
- var window = new StockForecastOrderScreen(items);
- if(window.ShowDialog() != true)
- {
- return false;
- }
-
- var orders = new List<Tuple<PurchaseOrder, List<PurchaseOrderItem>>>();
- Progress.ShowModal("Creating Orders", progress =>
- {
- int iOrder = 1;
- foreach(var perSupplier in window.Results.GroupBy(x => x.Supplier.ID))
- {
- progress.Report($"Creating Orders ({iOrder++}/{window.Results.ToArray().Length})");
- var order = new PurchaseOrder();
- order.Description = "Purchase Order created from Stock Forecast Screen";
- order.RaisedBy.ID = App.EmployeeID;
- LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, perSupplier.Key);
- var orderItems = new List<PurchaseOrderItem>();
- 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<PurchaseOrderItem, Product, ProductLink>(
- orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Product.ID)),
- x => x.Product);
- LookupFactory.DoLookups<PurchaseOrderItem, ProductStyle, ProductStyleLink>(
- orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Style.ID)),
- x => x.Style);
- LookupFactory.DoLookups<PurchaseOrderItem, Job, JobLink>(
- orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Job.ID)),
- x => x.Job);
- LookupFactory.DoLookups<PurchaseOrderItem, TaxCode, TaxCodeLink>(
- orderItems.WithIndex().Select(x => new Tuple<PurchaseOrderItem, Guid>(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.ForeignCurrencyCost = item.SupplierProduct.ForeignCurrencyPrice;
- orderItem.Cost = item.SupplierProduct.CostPrice;
- }
- orders.Add(new(order, orderItems));
- }
-
- progress.Report($"Saving {orders.Count} Orders");
- 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;
- MessageWindow.ShowMessage($"The following orders were created:\n- {string.Join("\n- ",orders.Select(x=>x.Item1.PONumber))}", $"Created {orders.Count} orders");
- 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
- }
|