StockForecastGrid.cs 37 KB

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