StockForecastGrid.cs 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.DynamicGrid;
  5. using InABox.Wpf;
  6. using InABox.WPF;
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using System.Linq.Expressions;
  11. using System.Threading;
  12. using System.Windows;
  13. using System.Windows.Controls;
  14. using System.Windows.Media;
  15. using System.Windows.Media.Imaging;
  16. using PRSDimensionUtils;
  17. using StockMovement = Comal.Classes.StockMovement;
  18. namespace PRSDesktop;
  19. public class StockForecastJobInfo
  20. {
  21. public string JobNumber { get; set; }
  22. public double BOM { get; set; }
  23. public double Stock { get; set; }
  24. public double PO { get; set; }
  25. public double Required => Math.Max(BOM - (Stock + PO), 0.0F);
  26. }
  27. public class StockForecastItem : BaseObject
  28. {
  29. public ProductLink Product => InitializeField(ref _product, nameof(Product));
  30. private ProductLink? _product;
  31. public ProductStyleLink Style => InitializeField(ref _style, nameof(Style));
  32. private ProductStyleLink? _style;
  33. public StockDimensions Dimensions => InitializeField(ref _dimensions, nameof(Dimensions));
  34. private StockDimensions? _dimensions;
  35. public bool IsProductInstance => ProductInstances.Count > 0;
  36. public List<ProductInstance> ProductInstances { get; set; } = new(0);
  37. public double Usage { get; set; }
  38. public int UsageDays { get; set; }
  39. public double UsageFactor { get; set; }
  40. public double StockFactor { get; set; }
  41. public bool CustomMinStock { get; set; }
  42. public double MinStock { get; set; }
  43. public double GenStock { get; set; }
  44. public double GenPO { get; set; }
  45. public double JobBOM { get; set; }
  46. public double JobStock { get; set; }
  47. public double JobPO { get; set; }
  48. public Dictionary<Guid, StockForecastJobInfo> JobInfo { get; private init; } = [];
  49. public void AddJobBOM(Guid jobID, double quantity)
  50. {
  51. var item = JobInfo.GetValueOrAdd(jobID);
  52. item.BOM += quantity;
  53. }
  54. public void AddJobPO(Guid jobID, double quantity)
  55. {
  56. var item = JobInfo.GetValueOrAdd(jobID);
  57. item.PO += quantity;
  58. }
  59. public void AddJobStock(Guid jobID, double quantity)
  60. {
  61. var item = JobInfo.GetValueOrAdd(jobID);
  62. item.Stock += quantity;
  63. }
  64. public double CalculatedMinimumStock => Math.Ceiling(CustomMinStock ? MinStock : Usage * StockFactor);
  65. public double StockRequired => Math.Max(CalculatedMinimumStock - (GenStock + GenPO), 0.0F);
  66. public double Required => Math.Max((CalculatedMinimumStock + JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
  67. public double Optimised => Math.Max(Math.Max(CalculatedMinimumStock, JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
  68. }
  69. public class StockForecastGrid : DynamicItemsListGrid<StockForecastItem>, IDataModelSource
  70. {
  71. private enum ColumnTag
  72. {
  73. UsageDays,
  74. Usage,
  75. MinimumStockRequired,
  76. GeneralStockHoldings,
  77. GeneralPurchaseOrders,
  78. JobStockRequired,
  79. JobStockHoldings,
  80. JobPurchaseOrders,
  81. BalanceRequired
  82. }
  83. private SupplierProduct[]? _supplierProducts = null;
  84. private static readonly BitmapImage _warning = InABox.Wpf.Resources.warning.AsBitmapImage();
  85. private static readonly BitmapImage _tick = InABox.Wpf.Resources.tick.AsBitmapImage();
  86. private static readonly BitmapImage _cart = PRSDesktop.Resources.purchase.AsBitmapImage();
  87. private static readonly BitmapImage _product = PRSDesktop.Resources.product.AsBitmapImage();
  88. public Guid[] GroupIDs
  89. {
  90. get => _groupIDs;
  91. set
  92. {
  93. SelectedForOrder.RemoveWhere(x => !value.Contains(x.Item1));
  94. _groupIDs = value;
  95. }
  96. }
  97. public Guid[] JobIDs { get; set; } = [];
  98. public HashSet<Guid> SupplierIDs { get; set; } = [];
  99. private readonly Button OrderButton;
  100. private HashSet<(Guid,Guid,Guid,String)> SelectedForOrder = [];
  101. private DynamicGridCustomColumnsComponent<StockForecastItem> ColumnsComponent;
  102. private string ColumnsTag => "StockForecastGrid";
  103. public StockForecastProperties? Properties { get; set; }
  104. public StockForecastGlobalProperties? GlobalProperties { get; set; }
  105. public event EventHandler OnPropertiesChanged;
  106. public StockForecastGrid() : base()
  107. {
  108. DimensionUtils.ResetDimensionScriptCache();
  109. ColumnsComponent = new DynamicGridCustomColumnsComponent<StockForecastItem>(this, ColumnsTag);
  110. HiddenColumns.Add(x => x.Product.ID);
  111. HiddenColumns.Add(x => x.Product.Issues);
  112. HiddenColumns.Add(x => x.Style.ID);
  113. HiddenColumns.Add(x => x.Dimensions.UnitSize);
  114. HiddenColumns.Add(x => x.Product.Image.ID);
  115. HiddenColumns.Add(x => x.Product.Image.FileName);
  116. HiddenColumns.Add(x => x.Product.Supplier.ID);
  117. HiddenColumns.Add(x => x.Product.Supplier.SupplierLink.ID);
  118. HiddenColumns.Add(x => x.Product.OrderStrategy);
  119. HiddenColumns.Add(x => x.Product.Group.ID);
  120. HiddenColumns.Add(x => x.Required);
  121. HiddenColumns.Add(x => x.Optimised);
  122. HiddenColumns.Add(x=>x.CustomMinStock);
  123. HiddenColumns.Add(x => x.UsageDays);
  124. HiddenColumns.Add(x => x.MinStock);
  125. HiddenColumns.Add(x => x.GenStock);
  126. HiddenColumns.Add(x => x.GenPO);
  127. HiddenColumns.Add(x => x.JobBOM);
  128. HiddenColumns.Add(x => x.JobStock);
  129. HiddenColumns.Add(x => x.JobPO);
  130. ActionColumns.Add(new DynamicImageColumn(ProductInstance_Image, null)
  131. {
  132. Position = DynamicActionColumnPosition.Start,
  133. ToolTip = ProductInstance_ToolTip
  134. });
  135. ActionColumns.Add(new DynamicImageColumn(Issues_Image, null)
  136. {
  137. ToolTip = Issues_Tooltip,
  138. Position = DynamicActionColumnPosition.Start
  139. });
  140. ActionColumns.Add(new DynamicImagePreviewColumn<StockForecastItem>(x => x.Product.Image)
  141. {
  142. Position = DynamicActionColumnPosition.Start
  143. });
  144. CreateColumn(GetPeriod, ColumnTag.UsageDays, "Days", "")
  145. .ContextMenu = ProductInstance_Menu;
  146. CreateColumn(GetAverage, ColumnTag.Usage,"Avg.","F2")
  147. .ContextMenu = ProductInstance_Menu;
  148. CreateColumn(GetMinimumStockLevel, ColumnTag.MinimumStockRequired,"Min.","F2")
  149. .ContextMenu = ProductInstance_Menu;
  150. CreateColumn(GetGeneralStockLevel, ColumnTag.GeneralStockHoldings,"Hld.","F2");
  151. CreateColumn(GetGeneralPurchaseOrder, ColumnTag.GeneralPurchaseOrders, "PO.","F2");
  152. CreateColumn(GetBOMBalance, ColumnTag.JobStockRequired, "BOM.","F2");
  153. CreateColumn(GetReservedStock, ColumnTag.JobStockHoldings, "Hld.","F2");
  154. CreateColumn(GetReservedPurchaseOrder, ColumnTag.JobPurchaseOrders, "PO.","F2");
  155. CreateColumn(GetBalanceRequired, ColumnTag.BalanceRequired,"Req.","");
  156. ActionColumns.Add(new DynamicImageColumn(SelectForOrder_Image, SelectForOrder_Click)
  157. {
  158. Position = DynamicActionColumnPosition.End
  159. });
  160. OrderButton = AddButton("Order Stock", _cart, OrderStock_Click);
  161. OrderButton.IsEnabled = false;
  162. }
  163. private FrameworkElement? ProductInstance_ToolTip(DynamicActionColumn column, CoreRow? row)
  164. {
  165. if(row is null)
  166. {
  167. return column.TextToolTip("Does each line match a product instance?");
  168. }
  169. else if (LoadItem(row).IsProductInstance)
  170. {
  171. return column.TextToolTip("This line matches a product instance.");
  172. }
  173. else
  174. {
  175. return column.TextToolTip("This line does not match a product instance.");
  176. }
  177. }
  178. private BitmapImage? ProductInstance_Image(CoreRow? row)
  179. {
  180. if(row is null || LoadItem(row).IsProductInstance)
  181. {
  182. return _product;
  183. }
  184. else
  185. {
  186. return null;
  187. }
  188. }
  189. #region Columns
  190. protected override DynamicGridColumns LoadColumns()
  191. {
  192. return ColumnsComponent.LoadColumns();
  193. }
  194. protected override void LoadColumnsMenu(ContextMenu menu)
  195. {
  196. ColumnsComponent.LoadColumnsMenu(menu);
  197. }
  198. protected override void SaveColumns(DynamicGridColumns columns)
  199. {
  200. ColumnsComponent.SaveColumns(columns);
  201. }
  202. #endregion
  203. private BitmapImage? Issues_Image(CoreRow? row)
  204. {
  205. return (row is null)
  206. ? _warning
  207. : row.Get<StockForecastItem, string>(x => x.Product.Issues).IsNullOrWhiteSpace()
  208. ? null
  209. : _warning;
  210. }
  211. private FrameworkElement? Issues_Tooltip(DynamicActionColumn column, CoreRow? row)
  212. {
  213. return (row is null)
  214. ? null
  215. : column.TextToolTip(row.Get<StockForecastItem, string>(x => x.Product.Issues));
  216. }
  217. #region UIComponent
  218. private UIComponent? _uicomponent = null;
  219. private Guid[] _groupIDs = [];
  220. private class UIComponent : DynamicGridGridUIComponent<StockForecastItem>
  221. {
  222. private StockForecastGrid Grid;
  223. public UIComponent(StockForecastGrid grid)
  224. {
  225. Grid = grid;
  226. Parent = grid;
  227. }
  228. public bool CheckSuppliers(CoreRow row)
  229. {
  230. if (Grid._supplierProducts == null)
  231. return false;
  232. var item = row.ToObject<StockForecastItem>(); //Grid.LoadItem(row));
  233. return Grid._supplierProducts.Any(r =>
  234. Equals(r.Product.ID, item.Product.ID)
  235. && Equals(r.Style.ID, item.Style.ID)
  236. //&& r.Dimensions.Unit.ID.Equals(item.Dimensions.Unit.ID)
  237. && Grid.SupplierIDs.Contains(r.SupplierLink.ID));
  238. }
  239. protected override Brush? GetCellForeground(CoreRow row, DynamicColumnBase column)
  240. {
  241. if (column is DynamicTextColumn { Tag: ColumnTag _tag } _col)
  242. {
  243. if (_tag == ColumnTag.UsageDays || _tag == ColumnTag.Usage || _tag == ColumnTag.MinimumStockRequired)
  244. {
  245. var item = Grid.LoadItem(row);
  246. if (!item.IsProductInstance)
  247. return Colors.Transparent.ToBrush();
  248. }
  249. }
  250. return base.GetCellForeground(row, column);
  251. }
  252. protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
  253. {
  254. if (column is DynamicTextColumn col)
  255. {
  256. var item = Grid.LoadItem(row);
  257. var avg = item.UsageDays > 0
  258. ? item.CalculatedMinimumStock.IsEffectivelyLessThan(item.Usage * item.StockFactor)
  259. ? Colors.LightSalmon.ToBrush(0.5)
  260. : Colors.LightGreen.ToBrush(0.5)
  261. : Colors.Gainsboro.ToBrush(0.5);
  262. var min = item.CustomMinStock
  263. ? Colors.Orchid.ToBrush(0.5)
  264. : avg;
  265. var stock = (item.GenStock + item.GenPO).IsEffectivelyLessThan(item.CalculatedMinimumStock)
  266. ? Colors.LightSalmon.ToBrush(0.5)
  267. : Colors.LightBlue.ToBrush(0.5);
  268. var job = Math.Max(0.0F, item.JobBOM - (item.JobStock + item.JobPO)).IsEffectivelyEqual(0.0F)
  269. ? Colors.LightGreen.ToBrush(0.5)
  270. : Colors.LightSalmon.ToBrush(0.5);
  271. var overall = !(Grid.Properties?.Optimise == true ? item.Optimised : item.Required).IsEffectivelyEqual(0.0F)
  272. ? Colors.LightSalmon.ToBrush(0.5)
  273. : null;
  274. return col.Tag switch
  275. {
  276. ColumnTag.UsageDays => avg,
  277. ColumnTag.Usage => avg,
  278. ColumnTag.MinimumStockRequired => min,
  279. ColumnTag.GeneralStockHoldings => stock,
  280. ColumnTag.GeneralPurchaseOrders => stock,
  281. ColumnTag.JobStockRequired => job,
  282. ColumnTag.JobStockHoldings => job,
  283. ColumnTag.JobPurchaseOrders => job,
  284. ColumnTag.BalanceRequired => overall,
  285. _ => null
  286. };
  287. }
  288. else
  289. {
  290. if (Grid.Properties?.AllStock == true && !CheckSuppliers(row))
  291. return Colors.Silver.ToBrush(0.5);
  292. }
  293. return null;
  294. }
  295. }
  296. protected override IDynamicGridUIComponent<StockForecastItem> CreateUIComponent()
  297. {
  298. return _uicomponent ??= new UIComponent(this);
  299. }
  300. #endregion
  301. protected override void DoReconfigure(DynamicGridOptions options)
  302. {
  303. base.DoReconfigure(options);
  304. options.Clear();
  305. options.RecordCount = true;
  306. options.SelectColumns = true;
  307. options.FilterRows = true;
  308. options.ExportData = true;
  309. options.MultiSelect = true;
  310. options.HideDatabaseFilters = true;
  311. }
  312. protected override void ConfigureColumnGroups()
  313. {
  314. base.ConfigureColumnGroups();
  315. AddColumnGrouping()
  316. .AddGroup("Usage", GetColumn(ColumnTag.UsageDays), GetColumn(ColumnTag.Usage))
  317. .AddGroup("General Stock", GetColumn(ColumnTag.MinimumStockRequired), GetColumn(ColumnTag.GeneralPurchaseOrders))
  318. .AddGroup("Job Stock", GetColumn(ColumnTag.JobStockRequired), GetColumn(ColumnTag.JobPurchaseOrders));
  319. }
  320. public override DynamicGridColumns GenerateColumns()
  321. {
  322. var columns = new DynamicGridColumns();
  323. columns.Add<StockForecastItem>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
  324. columns.Add<StockForecastItem>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
  325. columns.Add<StockForecastItem>(x => x.Style.Code, 120, "Style Code", "", Alignment.MiddleCenter);
  326. columns.Add<StockForecastItem>(x => x.Dimensions.UnitSize, 120, "Unit Size", "", Alignment.MiddleCenter);
  327. return columns;
  328. }
  329. #region Column Data and Details
  330. private DynamicTextColumn CreateColumn(DynamicTextColumn.GetTextDelegate calculate, ColumnTag tag, string header, string format)
  331. {
  332. var column = new DynamicTextColumn(calculate)
  333. {
  334. Width = 65,
  335. Format=format,
  336. Position = DynamicActionColumnPosition.End,
  337. Tag = tag,
  338. HeaderText = header,
  339. GetFilter = () => new DynamicColumnFilter<double?>(
  340. r => GetColumnCalculatedData(tag, r),
  341. filter => GetColumnFilterItems(filter, tag))
  342. };
  343. ActionColumns.Add(column);
  344. return column;
  345. }
  346. private DynamicTextColumn GetColumn(ColumnTag tag) => (ActionColumns.First(x => Equals(x.Tag, tag)) as DynamicTextColumn)!;
  347. private object GetAverage(CoreRow? row)
  348. {
  349. if (row is not null)
  350. {
  351. var item = LoadItem(row);
  352. if (item.IsProductInstance)
  353. return item.Usage;
  354. }
  355. return 0.0;
  356. }
  357. private object GetPeriod(CoreRow? row)
  358. {
  359. if (row is not null)
  360. {
  361. var item = LoadItem(row);
  362. if (item.IsProductInstance)
  363. return item.UsageDays;
  364. }
  365. return 0;
  366. }
  367. private object GetMinimumStockLevel(CoreRow? row)
  368. {
  369. if (row is not null)
  370. {
  371. var item = LoadItem(row);
  372. if (item.IsProductInstance)
  373. return item.CalculatedMinimumStock;
  374. }
  375. return 0.0;
  376. }
  377. private object GetGeneralStockLevel(CoreRow? row)
  378. => row is not null ? LoadItem(row).GenStock : 0.0;
  379. private object GetGeneralPurchaseOrder(CoreRow? row)
  380. => row is not null ? LoadItem(row).GenPO : 0.0;
  381. private object GetBOMBalance(CoreRow? row)
  382. => row is not null ? LoadItem(row).JobBOM : 0.0;
  383. private object GetReservedStock(CoreRow? row)
  384. => row is not null ? LoadItem(row).JobStock : 0.0;
  385. private object GetReservedPurchaseOrder(CoreRow? row)
  386. => row is not null ? LoadItem(row).JobPO : 0.0;
  387. private object GetBalanceRequired(CoreRow? row)
  388. {
  389. if(row is not null)
  390. {
  391. var item = LoadItem(row);
  392. var result = Properties?.Optimise == true
  393. ? (item.Optimised.IsEffectivelyEqual(0.0) ? "" : $"{item.Optimised:F2}")
  394. : (item.Required.IsEffectivelyEqual(0.0) ? "" : $"{item.Required:F2}");
  395. return result;
  396. }
  397. else
  398. {
  399. return "";
  400. }
  401. }
  402. private void ShowDetailGrid(String title, params Func<IDynamicDataGrid?>[] gridfuncs)
  403. {
  404. var _window = new ThemableWindow { Title = title };
  405. var _tabcontrol = new DynamicTabControl() { TabStripPlacement = Dock.Bottom, Margin = new Thickness(5) };
  406. _window.Content = _tabcontrol;
  407. foreach (var gridfunc in gridfuncs)
  408. {
  409. var _grid = gridfunc();
  410. if (_grid != null)
  411. {
  412. _tabcontrol.Items.Add(
  413. new DynamicTabItem()
  414. {
  415. Header = CoreUtils.Neatify(_grid.DataType.Name.Split('.').Last()),
  416. Content = _grid
  417. }
  418. );
  419. _grid.Refresh(true,true);
  420. }
  421. }
  422. _window.ShowDialog();
  423. }
  424. private IDynamicDataGrid BuildDetailGrid<TEntity>(
  425. String tag,
  426. Expression<Func<TEntity,object?>> productcol,
  427. Guid productid,
  428. Expression<Func<TEntity,object?>> stylecol,
  429. Guid? styleid,
  430. Expression<Func<TEntity, IDimensions>> dimcol,
  431. IDimensions? dimensions,
  432. Expression<Func<TEntity,object?>>? jobcol,
  433. Filter<TEntity>? extrafilter,
  434. Func<CoreRow,bool>? rowfilter
  435. )
  436. {
  437. var _grid = (Activator.CreateInstance(typeof(DynamicDataGrid<>).MakeGenericType(typeof(TEntity))) as IDynamicDataGrid);
  438. if (_grid == null)
  439. {
  440. MessageWindow.ShowError($"Cannot create Grid for [{typeof(TEntity).Name}]", "", shouldLog: false);
  441. return null;
  442. }
  443. _grid.ColumnsTag = $"{ColumnsTag}.{tag}";
  444. _grid.Reconfigure(options =>
  445. {
  446. options.Clear();
  447. options.FilterRows = true;
  448. options.SelectColumns = true;
  449. });
  450. _grid.OnDefineFilter += t =>
  451. {
  452. var _filter = new Filter<TEntity>(productcol).IsEqualTo(productid);
  453. if(dimensions is not null)
  454. _filter = _filter.And(CoreUtils.GetFullPropertyName(dimcol, ".")).DimensionEquals(dimensions);
  455. if (styleid.HasValue)
  456. _filter = _filter.And(stylecol).IsEqualTo(styleid);
  457. if (jobcol != null)
  458. _filter = _filter.And(new Filter<TEntity>(jobcol).InList(JobIDs));
  459. if (extrafilter != null)
  460. _filter = _filter.And(extrafilter);
  461. return _filter;
  462. };
  463. _grid.OnFilterRecord += row => rowfilter?.Invoke(row) ?? true;
  464. return _grid;
  465. }
  466. protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
  467. {
  468. //base.DoDoubleClick(sender, args);
  469. if (args.Row is null || args.Column?.Tag is not ColumnTag tag) return;
  470. var item = LoadItem(args.Row);
  471. var styleid = HasStyle() ? item.Style.ID : (Guid?)null;
  472. switch (tag)
  473. {
  474. case ColumnTag.Usage :
  475. ShowDetailGrid(
  476. "Stock Usage",
  477. () => BuildDetailGrid<StockMovement>(
  478. ColumnTag.Usage.ToString(),
  479. x => x.Product.ID,
  480. item.Product.ID,
  481. x => x.Style.ID,
  482. styleid,
  483. x => x.Dimensions,
  484. item.Dimensions,
  485. null,
  486. new Filter<StockMovement>(x=>x.Job.ID).IsEqualTo(Guid.Empty)
  487. .And(x=>x.Date).IsGreaterThanOrEqualTo(DateTime.Today.AddDays(0-item.UsageDays * item.UsageFactor))
  488. .And(x=>x.Type).InList(
  489. StockMovementType.Issue,
  490. StockMovementType.TransferOut,
  491. StockMovementType.TransferIn,
  492. StockMovementType.StockTake),
  493. null
  494. )
  495. );
  496. break;
  497. case ColumnTag.GeneralStockHoldings:
  498. ShowDetailGrid(
  499. "Stock Holdings",
  500. () => BuildDetailGrid<StockHolding>(
  501. ColumnTag.GeneralStockHoldings.ToString(),
  502. x => x.Product.ID,
  503. item.Product.ID,
  504. x => x.Style.ID,
  505. styleid,
  506. x => x.Dimensions,
  507. item.Dimensions,
  508. null,
  509. new Filter<StockHolding>(x=>x.Job.ID).IsEqualTo(Guid.Empty),
  510. null
  511. )
  512. );
  513. break;
  514. case ColumnTag.GeneralPurchaseOrders:
  515. //ShowDetailGrid(
  516. // "Purchase Order Allocations",
  517. // () => BuildDetailGrid<PurchaseOrderItemAllocation>(
  518. // ColumnTag.GeneralPurchaseOrders.ToString(),
  519. // x => x.Item.Product.ID,
  520. // item.Product.ID,
  521. // x => x.Item.Style.ID,
  522. // styleid,
  523. // x => x.Item.Dimensions,
  524. // item.Dimensions,
  525. // null,
  526. // new Filter<PurchaseOrderItemAllocation>(x=>x.Job.ID).IsEqualTo(Guid.Empty)
  527. // .And(x=>x.Item.ReceivedDate).IsEqualTo(DateTime.MinValue),
  528. // null
  529. // )
  530. //);
  531. break;
  532. case ColumnTag.JobStockRequired:
  533. ShowDetailGrid(
  534. "Bills Of Materials",
  535. () => BuildDetailGrid<JobBillOfMaterialsItem>(
  536. ColumnTag.JobStockRequired.ToString(),
  537. x => x.Product.ID,
  538. item.Product.ID,
  539. x => x.Style.ID,
  540. styleid,
  541. x => x.Dimensions,
  542. item.Dimensions,
  543. x => x.Job.ID,
  544. new Filter<JobBillOfMaterialsItem>(x=>x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue),
  545. null
  546. ),
  547. () => BuildDetailGrid<StockMovement>(
  548. "JobStockIssued",
  549. x => x.Product.ID,
  550. item.Product.ID,
  551. x => x.Style.ID,
  552. styleid,
  553. x => x.Dimensions,
  554. item.Dimensions,
  555. x => x.Job.ID,
  556. new Filter<StockMovement>(x=>x.Type).IsEqualTo(StockMovementType.Issue),
  557. null
  558. )
  559. );
  560. break;
  561. case ColumnTag.JobStockHoldings:
  562. ShowDetailGrid(
  563. "Stock Holdings",
  564. () => BuildDetailGrid<StockHolding>(
  565. ColumnTag.JobStockHoldings.ToString(),
  566. x => x.Product.ID,
  567. item.Product.ID,
  568. x => x.Style.ID,
  569. styleid,
  570. x => x.Dimensions,
  571. item.Dimensions,
  572. x => x.Job.ID,
  573. null,
  574. null
  575. )
  576. );
  577. break;
  578. case ColumnTag.JobPurchaseOrders:
  579. //ShowDetailGrid(
  580. // "Purchase Orders",
  581. // () => BuildDetailGrid<PurchaseOrderItemAllocation>(
  582. // ColumnTag.GeneralPurchaseOrders.ToString(),
  583. // x => x.Item.Product.ID,
  584. // item.Product.ID,
  585. // x => x.Item.Style.ID,
  586. // styleid,
  587. // x => x.Item.Dimensions,
  588. // item.Dimensions,
  589. // x => x.Job.ID,
  590. // new Filter<PurchaseOrderItemAllocation>(x => x.Item.ReceivedDate).IsEqualTo(DateTime.MinValue),
  591. // null
  592. // )
  593. //);
  594. break;
  595. }
  596. }
  597. private ContextMenu? ProductInstance_Menu(CoreRow[]? rows)
  598. {
  599. if (rows is null || rows.Length == 0 || !Security.CanEdit<ProductInstance>())
  600. return null;
  601. var _items = LoadItems(rows);
  602. if (_items.Any(x => x.IsProductInstance))
  603. {
  604. var _menu = new ContextMenu();
  605. _menu.AddItem("Edit ProductInstance", null, _items, EditProductInstance_Click);
  606. return _menu;
  607. }
  608. return null;
  609. }
  610. private DynamicDataGrid<ProductInstance>? _instanceGrid;
  611. private void EditProductInstance_Click(StockForecastItem[] sfItems)
  612. {
  613. var items = sfItems.SelectMany(x=>x.ProductInstances).ToArray();
  614. _instanceGrid ??= new DynamicDataGrid<ProductInstance>();
  615. if (_instanceGrid.EditItems(items))
  616. {
  617. Client.Save(items.Where(x=>x.IsChanged()),"Updated from Stock Forecast Screen");
  618. Refresh(false,true);
  619. }
  620. }
  621. #endregion
  622. #region Refresh
  623. private bool HasStyle()
  624. {
  625. return DataColumns().ColumnNames().Any(x => x.StartsWith("Style.") && !x.Equals("Style.ID"));
  626. }
  627. private CoreRow[] GetRows<TSource>(CoreTable table, Guid productid, Guid? styleid, IDimensions dimensions, Guid[] jobids) where TSource : IJobMaterial
  628. {
  629. int productcol = table.GetColumnIndex<TSource>(x => x.Product.ID);
  630. int stylecol = styleid.HasValue ? table.GetColumnIndex<TSource>(x => x.Style.ID) : -1;
  631. var dimCols = Dimensions.GetFilterColumnIndices<TSource>(table, x => x.Dimensions);
  632. int jobcol = table.GetColumnIndex<TSource>(x => x.Job.ID);
  633. var subset = table.Rows
  634. .Where(r =>
  635. Guid.Equals(r.Values[productcol], productid)
  636. && (!styleid.HasValue || Guid.Equals(r.Values[stylecol], styleid))
  637. && r.ToDimensions<StockDimensions>(dimCols).Equals(dimensions)
  638. && jobids.Any(x=>Equals(x,r.Values[jobcol]))
  639. );
  640. return subset.ToArray();
  641. }
  642. 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)
  643. {
  644. int srcol = table.GetColumnIndex(source);
  645. if (srcol == -1)
  646. return 0.00;
  647. var total = rows.Aggregate(0d, (value, row) => value + (double)(row.Values[srcol] ?? 0.0d));
  648. // int productcol = columns.IndexOf(x => x.Product.ID);
  649. // int stylecol = hasstyle ? columns.IndexOf(x => x.Style.ID) : -1;
  650. // int jobcol = hasjob ? columns.IndexOf(x => x.Job.ID) : -1;
  651. // int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize);
  652. //
  653. // var tuples = rows.Select(r => new Tuple<Guid, Guid?, Guid?, String, double>(
  654. // (Guid)(r.Values[productcol] ?? Guid.Empty),
  655. // (hasstyle ? (Guid)(r.Values[stylecol] ?? Guid.Empty) : null),
  656. // (hasjob ? (Guid)(r.Values[jobcol] ?? Guid.Empty) : null),
  657. // (String)(r.Values[unitcol] ?? ""),
  658. // (double)(r.Values[aggcol] ?? 0.0d))
  659. // ).ToArray();
  660. //
  661. // var total = tuples.Aggregate(0d, (value, tuple) => value + tuple.Item5);
  662. if(aggregate is not null)
  663. {
  664. target?.Set(aggregate, total);
  665. }
  666. return total;
  667. }
  668. private double? GetColumnCalculatedData(ColumnTag tag, CoreRow row)
  669. {
  670. return tag switch
  671. {
  672. ColumnTag.Usage => row.Get<StockForecastItem, double>(x => x.Usage),
  673. ColumnTag.UsageDays => row.Get<StockForecastItem,int>(x => x.UsageDays),
  674. ColumnTag.MinimumStockRequired => row.Get<StockForecastItem, double>(x => x.CalculatedMinimumStock),
  675. ColumnTag.GeneralStockHoldings => row.Get<StockForecastItem, double>(x => x.GenStock),
  676. ColumnTag.GeneralPurchaseOrders => row.Get<StockForecastItem, double>(x => x.GenPO),
  677. ColumnTag.JobStockRequired => row.Get<StockForecastItem, double>(x => x.JobBOM),
  678. ColumnTag.JobStockHoldings => row.Get<StockForecastItem, double>(x => x.JobStock),
  679. ColumnTag.JobPurchaseOrders => row.Get<StockForecastItem, double>(x => x.JobPO),
  680. ColumnTag.BalanceRequired => (Properties?.Optimise == true ? row.Get<StockForecastItem, double>(x => x.Optimised) : row.Get<StockForecastItem, double>(x => x.Required)),
  681. _ => null
  682. };
  683. }
  684. private IEnumerable<Tuple<string, double?>> GetColumnFilterItems(DynamicColumnFilter<double?> filter, ColumnTag tag)
  685. {
  686. var items = new HashSet<double>();
  687. foreach(var row in (filter as IDynamicGridColumnFilter).GetRowsToFilter(this))
  688. {
  689. var value = GetColumnCalculatedData(tag, row);
  690. if (value.HasValue && !items.Contains(value.Value))
  691. {
  692. items.Add(value.Value);
  693. }
  694. }
  695. var arr = items.ToArray();
  696. Array.Sort(arr);
  697. return arr.Select(x => new Tuple<string, double?>(x.ToString("F2"), x));
  698. }
  699. private class ItemKey(Guid productID, Guid styleID, StockDimensions dimensions)
  700. {
  701. public Guid ProductID { get; set; } = productID;
  702. public Guid StyleID { get; set; } = styleID;
  703. public StockDimensions Dimensions { get; set; } = dimensions;
  704. public override bool Equals(object? obj)
  705. {
  706. return obj is ItemKey other
  707. && ProductID == other.ProductID
  708. && StyleID == other.StyleID
  709. && Dimensions.Equals(other.Dimensions);
  710. }
  711. public override int GetHashCode()
  712. {
  713. return HashCode.Combine(ProductID, StyleID, Dimensions);
  714. }
  715. }
  716. protected override void Reload(Filters<StockForecastItem> criteria, Columns<StockForecastItem> columns, ref SortOrder<StockForecastItem>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
  717. {
  718. // Need to query ProductInstances, StockHoldings, Job BOM and PO.
  719. KeyedQueryDef<T> GetQuery<T>(Filter<T>? filter = null, Columns<T>? columns = null) where T : Entity, IJobMaterial, IRemotable, IPersistent, new()
  720. {
  721. return new KeyedQueryDef<T>(
  722. Filter<T>.And(
  723. new Filter<T>(x => x.Product.Group.ID).InList(GroupIDs)
  724. .And(new Filter<T>(x => x.Job.ID).InList(JobIDs)
  725. .Or(x => x.Job.ID).IsEqualTo(Guid.Empty)),
  726. filter),
  727. Columns.None<T>()
  728. .Add(x => x.Product.ID)
  729. .Add(x => x.Product.Group.ID)
  730. .Add(x => x.Job.ID)
  731. .Add(x => x.Style.ID)
  732. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data)
  733. .Add(x => x.Dimensions.UnitSize)
  734. .Add(x => x.Dimensions.Value)
  735. .AddRange(columns ?? Enumerable.Empty<Column<T>>()));
  736. }
  737. ItemKey GetKeyFromRow(CoreRow row)
  738. {
  739. var key = new ItemKey(
  740. row.Get<IJobMaterial, Guid>(x => x.Product.ID),
  741. row.Get<IJobMaterial, Guid>(x => x.Style.ID),
  742. row.ToDimensions<IJobMaterial, StockDimensions>(x => x.Dimensions));
  743. key.Dimensions.UnitSize = row.Get<IJobMaterial, string>(x => x.Dimensions.UnitSize);
  744. key.Dimensions.Value = row.Get<IJobMaterial, double>(x => x.Dimensions.Value);
  745. return key;
  746. }
  747. ItemKey GetKey(Guid productid, Guid styleid, StockDimensions dimensions)
  748. {
  749. var key = new ItemKey(
  750. productid,
  751. styleid,
  752. dimensions);
  753. return key;
  754. }
  755. var queries = new IKeyedQueryDef[]
  756. {
  757. new KeyedQueryDef<ProductInstance>(
  758. new Filter<ProductInstance>(x => x.Product.Group.ID).InList(GroupIDs),
  759. //new Filter<ProductInstance>(x=>x.MinimumStockLevel).IsNotEqualTo(0.0).And(x => x.Product.Group.ID).InList(GroupIDs),
  760. Columns.None<ProductInstance>()
  761. .Add(x => x.ID)
  762. .Add(x => x.Product.ID)
  763. .Add(x => x.Style.ID)
  764. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data)
  765. .Add(x => x.Dimensions.UnitSize)
  766. .Add(x => x.Dimensions.Value)
  767. .Add(x => x.MinimumStock.CustomUsage)
  768. .Add(x=>x.MinimumStock.UsageDays)
  769. .Add(x=>x.MinimumStock.UsageFactor)
  770. .Add(x=>x.MinimumStock.StockFactor)
  771. .Add(x=>x.MinimumStock.CustomStockLevel)
  772. .Add(x=>x.MinimumStock.StockLevel)
  773. ),
  774. GetQuery<StockHolding>(
  775. columns: Columns.None<StockHolding>()
  776. .Add(x => x.Units)
  777. ),
  778. new KeyedQueryDef<PurchaseOrderItem>(
  779. new Filter<PurchaseOrderItem>(x => x.ReceivedDate).IsEqualTo(DateTime.MinValue)
  780. .And(x => x.Product.Group.ID).InList(GroupIDs),
  781. Columns.None<PurchaseOrderItem>()
  782. .Add(x => x.ID)
  783. .Add(x => x.Qty)
  784. .Add(x => x.Product.ID)
  785. .Add(x => x.Style.ID)
  786. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Data)
  787. .Add(x => x.Dimensions.UnitSize)
  788. .Add(x => x.Dimensions.Value)
  789. ),
  790. new KeyedQueryDef<PurchaseOrderItemAllocation>(
  791. new Filter<PurchaseOrderItemAllocation>(x => x.Item.ReceivedDate).IsEqualTo(DateTime.MinValue)
  792. .And(x => x.Item.Product.Group.ID).InList(GroupIDs)
  793. .And(new Filter<PurchaseOrderItemAllocation>(x => x.Job.ID).InList(JobIDs)
  794. .Or(x => x.Job.ID).IsEqualTo(Guid.Empty)),
  795. Columns.None<PurchaseOrderItemAllocation>()
  796. .Add(x => x.Quantity)
  797. .Add(x => x.Job.ID)
  798. .Add(x => x.Item.ID)
  799. .Add(x => x.Item.Product.ID)
  800. .Add(x => x.Item.Style.ID)
  801. .AddDimensionsColumns(x => x.Item.Dimensions, Dimensions.ColumnsType.Data)
  802. .Add(x => x.Item.Dimensions.UnitSize)
  803. .Add(x => x.Item.Dimensions.Value)
  804. ),
  805. GetQuery<JobBillOfMaterialsItem>(
  806. filter: new Filter<JobBillOfMaterialsItem>(x=>x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue),
  807. columns: Columns.None<JobBillOfMaterialsItem>().Add(x => x.Quantity)
  808. ),
  809. GetQuery<StockMovement>(
  810. filter: new Filter<StockMovement>(x => x.Type).InList(
  811. StockMovementType.Issue,
  812. StockMovementType.TransferOut,
  813. StockMovementType.TransferIn,
  814. StockMovementType.StockTake),
  815. columns: Columns.None<StockMovement>()
  816. .Add(x=>x.Date)
  817. .Add(x => x.Units)
  818. ),
  819. GetQuery<SupplierProduct>(
  820. columns: Columns.None<SupplierProduct>()
  821. .Add(x => x.SupplierLink.ID)
  822. .Add(x => x.CostPrice)
  823. .Add(x => x.Discount)
  824. .Add(x => x.TaxCode.ID)
  825. )
  826. };
  827. Client.QueryMultiple(
  828. (QueryMultipleResults? results, Exception? error) =>
  829. {
  830. if (results is null)
  831. {
  832. action(null, error);
  833. return;
  834. }
  835. var items = new Dictionary<ItemKey, StockForecastItem>();
  836. StockForecastItem GetItem(ItemKey key)
  837. {
  838. if(!items.TryGetValue(key, out var item))
  839. {
  840. item = new StockForecastItem();
  841. item.Product.ID = key.ProductID;
  842. item.Style.ID = key.StyleID;
  843. item.Dimensions.CopyFrom(key.Dimensions);
  844. items[key] = item;
  845. }
  846. return item;
  847. }
  848. var productInstances = results.GetArray<ProductInstance>();
  849. foreach(var instance in productInstances)
  850. {
  851. var dimensions = new StockDimensions();
  852. dimensions.CopyFrom(instance.Dimensions);
  853. var minStock = instance.MinimumStock.CustomStockLevel ? instance.MinimumStock.StockLevel : 0.0;
  854. var minCost = instance.AverageCost;
  855. DimensionUtils.ConvertDimensions(dimensions, ref minStock, ref minCost, Client<ProductDimensionUnit>.Provider);
  856. var item = GetItem(new(instance.Product.ID, instance.Style.ID, dimensions));
  857. item.UsageDays = Math.Max(item.UsageDays,instance.MinimumStock.CustomUsage ? instance.MinimumStock.UsageDays : (GlobalProperties?.UsageDays ?? 0));
  858. item.UsageFactor = Math.Max(item.UsageFactor, instance.MinimumStock.CustomUsage ? instance.MinimumStock.UsageFactor : (GlobalProperties?.UsageFactor ?? 0));
  859. item.StockFactor = Math.Max(item.StockFactor, instance.MinimumStock.CustomUsage ? instance.MinimumStock.StockFactor : (GlobalProperties?.StockFactor ?? 0));
  860. item.CustomMinStock = item.CustomMinStock || instance.MinimumStock.CustomStockLevel;
  861. item.MinStock += minStock;
  862. item.ProductInstances.Add(instance);
  863. }
  864. var holdings = results.Get<StockHolding>();
  865. foreach(var holdingrow in holdings.Rows)
  866. {
  867. var holding = holdingrow.ToObject<StockHolding>();
  868. holding.ConvertDimensions(x => x.Dimensions, x => x.Units, x => x.AverageValue,
  869. Client<ProductDimensionUnit>.Provider);
  870. var item = GetItem(GetKey(holding.Product.ID, holding.Style.ID, holding.Dimensions));
  871. if(holding.Job.ID == Guid.Empty)
  872. {
  873. item.GenStock += holding.Units;
  874. }
  875. else
  876. {
  877. item.JobStock += holding.Units;
  878. item.AddJobStock(holding.Job.ID, holding.Units);
  879. }
  880. }
  881. var purchaseOrderItems = results.GetObjects<PurchaseOrderItem>()
  882. .ToDictionary(x => x.ID);
  883. foreach (var poi in purchaseOrderItems.Values)
  884. {
  885. poi.ConvertDimensions(x => x.Dimensions, x => x.Qty, x => x.Cost,
  886. Client<ProductDimensionUnit>.Provider);
  887. }
  888. var allocations = results.Get<PurchaseOrderItemAllocation>();
  889. foreach(var allocationrow in allocations.Rows)
  890. {
  891. var allocation = allocationrow.ToObject<PurchaseOrderItemAllocation>();
  892. // POIAs are already converted where necessary, so we don't have to update the quantities again, just update the dimensions
  893. DimensionUtils.ConvertDimensions(allocation.Item.Dimensions, allocation.Quantity, allocation.Item.Cost, Client<ProductDimensionUnit>.Provider);
  894. var key = new ItemKey(
  895. allocation.Item.Product.ID,
  896. allocation.Item.Style.ID,
  897. allocation.Item.Dimensions);
  898. var item = GetItem(key);
  899. if(allocation.Job.ID == Guid.Empty)
  900. {
  901. item.GenPO += allocation.Quantity;
  902. }
  903. else
  904. {
  905. item.JobPO += allocation.Quantity;
  906. item.AddJobPO(allocation.Job.ID, allocation.Quantity);
  907. }
  908. if(purchaseOrderItems.TryGetValue(allocation.Item.ID, out var poi))
  909. poi.Qty -= allocation.Quantity;
  910. }
  911. foreach(var poi in purchaseOrderItems.Values)
  912. {
  913. var key = new ItemKey(
  914. poi.Product.ID,
  915. poi.Style.ID,
  916. poi.Dimensions);
  917. var item = GetItem(key);
  918. if(poi.Job.ID == Guid.Empty)
  919. {
  920. item.GenPO += poi.Qty;
  921. }
  922. else
  923. {
  924. item.JobPO += poi.Qty;
  925. item.AddJobPO(poi.Job.ID, poi.Qty);
  926. }
  927. }
  928. var jobBOMItems = results.Get<JobBillOfMaterialsItem>();
  929. foreach(var bomItemRow in jobBOMItems.Rows)
  930. {
  931. var bomItem = bomItemRow.ToObject<JobBillOfMaterialsItem>();
  932. bomItem.ConvertDimensions(x => x.Dimensions, x => x.Quantity, x => x.UnitCost, Client<ProductDimensionUnit>.Provider);
  933. var key = GetKey(bomItem.Product.ID, bomItem.Style.ID, bomItem.Dimensions);
  934. var item = GetItem(key);
  935. item.AddJobBOM(bomItem.Job.ID, bomItem.Quantity);
  936. }
  937. var movements = results.Get<StockMovement>();
  938. foreach(var mvtrow in movements.Rows)
  939. {
  940. var movement = mvtrow.ToObject<StockMovement>();
  941. movement.ConvertDimensions(x => x.Dimensions, x => x.Units, x => x.Cost,
  942. Client<ProductDimensionUnit>.Provider);
  943. var item = GetItem(GetKey(movement.Product.ID, movement.Style.ID, movement.Dimensions));
  944. if (movement.Job.ID == Guid.Empty)
  945. {
  946. var _days = item.UsageDays * item.UsageFactor;
  947. if (!_days.IsEffectivelyEqual(0.0) && DateTime.Today.Subtract(movement.Date) <= TimeSpan.FromDays(_days))
  948. item.Usage += (0.0 - movement.Units) / item.UsageFactor;
  949. }
  950. else
  951. item.AddJobBOM(movement.Job.ID, movement.Units);
  952. }
  953. _supplierProducts = results.GetArray<SupplierProduct>();
  954. Items = items.Values.ToList();
  955. foreach(var item in Items)
  956. {
  957. foreach(var (job, info) in item.JobInfo)
  958. {
  959. info.BOM = Math.Max(info.BOM, 0.0);
  960. }
  961. item.JobBOM = item.JobInfo.Sum(x => x.Value.BOM);
  962. }
  963. Items.LoadForeignProperties(columns);
  964. Items.Sort((a, b) => a.Product.Code.CompareTo(b.Product.Code));
  965. var result = new CoreTable();
  966. result.LoadColumns(columns);
  967. result.LoadRows(Items);
  968. action(result, null);
  969. }, queries);
  970. }
  971. protected override bool FilterRecord(CoreRow row)
  972. {
  973. bool result = base.FilterRecord(row);
  974. if (Properties?.RequiredOnly == true)
  975. {
  976. result = result && Properties?.Optimise == true
  977. ? !row.Get<StockForecastItem, double>(x => x.Optimised).IsEffectivelyEqual(0)
  978. : !row.Get<StockForecastItem, double>(x => x.Required).IsEffectivelyEqual(0);
  979. }
  980. if (Properties?.AllStock != true)
  981. result = result && _uicomponent?.CheckSuppliers(row) == true;
  982. return result;
  983. }
  984. #endregion
  985. #region Ordering
  986. private IEnumerable<CoreRow> FilterRows(IEnumerable<CoreRow> rows)
  987. {
  988. var predicates = GetFilterPredicates();
  989. return rows.Where(r =>
  990. {
  991. return predicates.All(x => x.Item2(r));
  992. });
  993. }
  994. private bool SelectForOrder_Click(CoreRow? row)
  995. {
  996. if (row is null)
  997. {
  998. var menu = new ContextMenu();
  999. menu.AddItem("Select all", null, () =>
  1000. {
  1001. foreach (var row in FilterRows(Data.Rows))
  1002. {
  1003. SelectedForOrder.Add(
  1004. RowToValueTuple(row)
  1005. );
  1006. InvalidateRow(row);
  1007. }
  1008. OrderButton.IsEnabled = SelectedForOrder.Count > 0;
  1009. });
  1010. menu.AddItem("Deselect all", null, () =>
  1011. {
  1012. SelectedForOrder.Clear();
  1013. InvalidateGrid();
  1014. OrderButton.IsEnabled = false;
  1015. });
  1016. menu.IsOpen = true;
  1017. return false;
  1018. }
  1019. else
  1020. {
  1021. var vt = RowToValueTuple(row);
  1022. if (!SelectedForOrder.Remove(vt))
  1023. {
  1024. SelectedForOrder.Add(vt);
  1025. }
  1026. OrderButton.IsEnabled = SelectedForOrder.Count > 0;
  1027. InvalidateRow(row);
  1028. return false;
  1029. }
  1030. }
  1031. private static (Guid,Guid, Guid, string) RowToValueTuple(CoreRow row)
  1032. {
  1033. return (
  1034. row.Get<StockForecastItem,Guid>(x=>x.Product.Group.ID),
  1035. row.Get<StockForecastItem,Guid>(x=>x.Product.ID),
  1036. row.Get<StockForecastItem,Guid>(x=>x.Style.ID),
  1037. row.Get<StockForecastItem,String>(x=>x.Dimensions.UnitSize)
  1038. );
  1039. }
  1040. private BitmapImage? SelectForOrder_Image(CoreRow? row)
  1041. {
  1042. if(row is null)
  1043. {
  1044. return _cart;
  1045. }
  1046. else if(SelectedForOrder.Contains(RowToValueTuple(row)))
  1047. {
  1048. return _cart;
  1049. }
  1050. else
  1051. {
  1052. return null;
  1053. }
  1054. }
  1055. private bool OrderStock_Click(Button button, CoreRow[] rows)
  1056. {
  1057. rows = FilterRows(Data.Rows.Where(x => SelectedForOrder.Contains(RowToValueTuple(x)))).ToArray();
  1058. if(rows.Length == 0)
  1059. {
  1060. return false;
  1061. }
  1062. var items = new List<StockForecastOrderData>();
  1063. foreach(var forecastItem in LoadItems(rows))
  1064. {
  1065. var item = new StockForecastOrderData(forecastItem.Product, forecastItem.Style, forecastItem.Dimensions);
  1066. item.RequiredQuantity = Properties?.Optimise == true ? forecastItem.Optimised : forecastItem.Required;
  1067. if(forecastItem.StockRequired > 0)
  1068. {
  1069. item.SetRequiredQuantity(Guid.Empty, Guid.Empty, "", forecastItem.StockRequired);
  1070. }
  1071. foreach(var (id, jobInfo) in forecastItem.JobInfo)
  1072. {
  1073. if (jobInfo.Required > 0)
  1074. item.SetRequiredQuantity(id, Guid.Empty, jobInfo.JobNumber, jobInfo.Required);
  1075. }
  1076. items.Add(item);
  1077. }
  1078. var window = new StockForecastOrderScreen(items) { Owner = App.Current.MainWindow };
  1079. if(window.ShowDialog() != true)
  1080. {
  1081. return false;
  1082. }
  1083. window.CreateOrders("Stock Forecast");
  1084. SelectedForOrder.Clear();
  1085. OrderButton.IsEnabled = false;
  1086. return true;
  1087. }
  1088. #endregion
  1089. #region IDataModelSource
  1090. public event DataModelUpdateEvent? OnUpdateDataModel;
  1091. public string SectionName => "Stock Forecast";
  1092. public DataModel DataModel(Selection selection)
  1093. {
  1094. return new AutoDataModel<ProductInstance>(null);
  1095. }
  1096. #endregion
  1097. }