StockForecastGrid.cs 39 KB

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