StockForecastGrid.cs 40 KB

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