StockForecastGrid.cs 40 KB

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