StockForecastGrid.cs 39 KB

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