| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091 |
- 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,
- 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(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 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)
- {
- // 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
- }
|