StockSummaryGrid.cs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926
  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.StockSummary.OrderScreen;
  17. using Syncfusion.UI.Xaml.Diagram.Controls;
  18. using Syncfusion.Windows.PdfViewer;
  19. namespace PRSDesktop;
  20. public class StockSummaryGrid : 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 StockSummaryGrid() : base()
  46. {
  47. ColumnsTag = "StockSummaryGrid";
  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<StockSummary>(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 StockSummaryGrid Grid;
  100. public UIComponent(StockSummaryGrid 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 (Grid.AllStock && !CheckSuppliers(row))
  140. return new SolidColorBrush(Colors.Silver) { Opacity = 0.5F };
  141. if (column is DynamicTextColumn col && Grid._summaryinfo.TryGetValue(row.Get<ProductInstance,Guid>(x=>x.ID), out StockSummaryInfo? info))
  142. {
  143. var stock = Math.Max(0.0F, info.MinStock - (info.GenStock + info.GenPO)).IsEffectivelyEqual(0.0F)
  144. ? new SolidColorBrush(Colors.LightBlue) { Opacity = 0.5 }
  145. : new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 };
  146. var job = Math.Max(0.0F, info.JobBOM - (info.JobStock + info.JobPO)).IsEffectivelyEqual(0.0F)
  147. ? new SolidColorBrush(Colors.LightGreen) { Opacity = 0.5 }
  148. : new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 };
  149. var overall = !(Grid.Optimise ? info.Optimised : info.Required).IsEffectivelyEqual(0.0F)
  150. ? new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 }
  151. : null;
  152. return col.Tag switch
  153. {
  154. ColumnTag.MinimumStockRequired => stock,
  155. ColumnTag.GeneralStockHoldings => stock,
  156. ColumnTag.GeneralPurchaseOrders => stock,
  157. ColumnTag.JobStockRequired => job,
  158. ColumnTag.JobStockHoldings => job,
  159. ColumnTag.JobPurchaseOrders => job,
  160. ColumnTag.BalanceRequired => overall,
  161. _ => null
  162. };
  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(FluentList<DynamicGridOption> options)
  173. {
  174. base.DoReconfigure(options);
  175. options
  176. .BeginUpdate()
  177. .Clear()
  178. .Add(DynamicGridOption.RecordCount)
  179. .Add(DynamicGridOption.SelectColumns)
  180. .Add(DynamicGridOption.FilterRows)
  181. .Add(DynamicGridOption.ExportData)
  182. .Add(DynamicGridOption.MultiSelect)
  183. .EndUpdate();
  184. }
  185. public override DynamicGridColumns GenerateColumns()
  186. {
  187. var columns = new DynamicGridColumns();
  188. columns.Add<ProductInstance, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
  189. columns.Add<ProductInstance, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
  190. columns.Add<ProductInstance, string>(x => x.Style.Code, 120, "Style Code", "", Alignment.MiddleCenter);
  191. columns.Add<ProductInstance, string>(x => x.Dimensions.UnitSize, 120, "Unit Size", "", Alignment.MiddleCenter);
  192. return columns;
  193. }
  194. #region Column Data and Details
  195. private void CreateColumn(DynamicTextColumn.GetTextDelegate calculate, ColumnTag tag, string header, string format)
  196. {
  197. var column = new DynamicTextColumn(calculate)
  198. {
  199. Width = 60,
  200. Format=format,
  201. Position = DynamicActionColumnPosition.End,
  202. Tag = tag,
  203. HeaderText = header
  204. };
  205. ActionColumns.Add(column);
  206. }
  207. private object GetMinimumStockLevel(CoreRow? row) => row?.Get<ProductInstance, double>(x => x.MinimumStockLevel) ?? 0.0F;
  208. private object GetGeneralStockLevel(CoreRow? row)
  209. {
  210. if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  211. return info.GenStock;
  212. return 0.0F;
  213. }
  214. private object GetGeneralPurchaseOrder(CoreRow? row)
  215. {
  216. if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  217. return info.GenPO;
  218. return 0.0F;
  219. }
  220. private object GetBOMBalance(CoreRow? row)
  221. {
  222. if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  223. return info.JobBOM;
  224. return 0.0F;
  225. }
  226. private object GetReservedStock(CoreRow? row)
  227. {
  228. if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  229. return info.JobStock;
  230. return 0.0F;
  231. }
  232. private object GetReservedPurchaseOrder(CoreRow? row)
  233. {
  234. if (row != null && _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  235. return info.JobPO;
  236. return 0.0F;
  237. }
  238. private object GetBalanceRequired(CoreRow? row)
  239. {
  240. if (row != null &&
  241. _summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  242. return Optimise
  243. ? info.Optimised.IsEffectivelyEqual(0.0F) ? "" : $"{info.Optimised:F2}"
  244. : info.Required.IsEffectivelyEqual(0.0F)
  245. ? ""
  246. : $"{info.Required:F2}";
  247. return "";
  248. }
  249. private void ShowDetailGrid<TEntity>(
  250. String tag,
  251. Expression<Func<TEntity,object?>> productcol,
  252. Guid productid,
  253. Expression<Func<TEntity,object?>> stylecol,
  254. Guid? styleid,
  255. Expression<Func<TEntity,object?>> unitcol,
  256. String unitsize,
  257. Expression<Func<TEntity,object?>>? jobcol,
  258. Filter<TEntity>? extrafilter,
  259. Func<CoreRow,bool>? rowfilter
  260. )
  261. {
  262. var grid = (Activator.CreateInstance(typeof(DynamicDataGrid<>).MakeGenericType(typeof(TEntity))) as IDynamicDataGrid);
  263. if (grid == null)
  264. {
  265. MessageWindow.ShowError($"Cannot create Grid for [{typeof(TEntity).Name}]", "", shouldLog: false);
  266. return;
  267. }
  268. grid.ColumnsTag = $"{ColumnsTag}.{tag}";
  269. grid.Reconfigure(options => { options.BeginUpdate().Clear().AddRange(DynamicGridOption.FilterRows, DynamicGridOption.SelectColumns).EndUpdate(); });
  270. grid.OnDefineFilter += t =>
  271. {
  272. var filter = new Filter<TEntity>(productcol).IsEqualTo(productid)
  273. .And(unitcol).IsEqualTo(unitsize);
  274. if (styleid.HasValue)
  275. filter = filter.And(stylecol).IsEqualTo(styleid);
  276. if (jobcol != null)
  277. filter = filter.And(new Filter<TEntity>(jobcol).InList(JobIDs));
  278. if (extrafilter != null)
  279. filter = filter.And(extrafilter);
  280. return filter;
  281. };
  282. grid.OnFilterRecord += row => rowfilter?.Invoke(row) ?? true;
  283. var window = DynamicGridUtils.CreateGridWindow($"Viewing {CoreUtils.Neatify(tag)} Calculation", grid);
  284. window.ShowDialog();
  285. }
  286. protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
  287. {
  288. //base.DoDoubleClick(sender, args);
  289. Guid productid = args?.Row?.Get<StockSummary, Guid>(c => c.Product.ID) ?? Guid.Empty;
  290. Guid? styleid = HasStyle() ? args?.Row?.Get<StockSummary, Guid>(c => c.Style.ID) : null;
  291. String unitsize = args?.Row?.Get<StockSummary, String>(c => c.Dimensions.UnitSize) ?? "";
  292. if (Equals(args?.Column?.Tag, ColumnTag.GeneralStockHoldings))
  293. {
  294. ShowDetailGrid<StockHolding>(
  295. ColumnTag.GeneralStockHoldings.ToString(),
  296. x => x.Product.ID,
  297. productid,
  298. x => x.Style.ID,
  299. styleid,
  300. x=>x.Dimensions.UnitSize,
  301. unitsize,
  302. null,
  303. new Filter<StockHolding>(x=>x.Job.ID).IsEqualTo(Guid.Empty),
  304. null
  305. );
  306. }
  307. else if (Equals(args?.Column?.Tag, ColumnTag.GeneralPurchaseOrders))
  308. {
  309. ShowDetailGrid<PurchaseOrderItem>(
  310. ColumnTag.GeneralPurchaseOrders.ToString(),
  311. x => x.Product.ID,
  312. productid,
  313. x => x.Style.ID,
  314. styleid,
  315. x=>x.Dimensions.UnitSize,
  316. unitsize,
  317. null,
  318. new Filter<PurchaseOrderItem>(x=>x.Job.ID).IsEqualTo(Guid.Empty)
  319. .And(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue),
  320. null
  321. );
  322. }
  323. else if (Equals(args?.Column?.Tag, ColumnTag.JobStockRequired))
  324. {
  325. ShowDetailGrid<JobBillOfMaterialsItem>(
  326. ColumnTag.JobStockRequired.ToString(),
  327. x => x.Product.ID,
  328. productid,
  329. x => x.Style.ID,
  330. styleid,
  331. x=>x.Dimensions.UnitSize,
  332. unitsize,
  333. x => x.Job.ID,
  334. new Filter<JobBillOfMaterialsItem>(x=>x.BillOfMaterials.Approved).IsNotEqualTo(DateTime.MinValue),
  335. null
  336. );
  337. }
  338. else if (Equals(args?.Column?.Tag, ColumnTag.JobStockHoldings))
  339. {
  340. ShowDetailGrid<StockHolding>(
  341. ColumnTag.JobStockHoldings.ToString(),
  342. x => x.Product.ID,
  343. productid,
  344. x => x.Style.ID,
  345. styleid,
  346. x=>x.Dimensions.UnitSize,
  347. unitsize,
  348. x => x.Job.ID,
  349. null,
  350. null
  351. );
  352. }
  353. else if (Equals(args?.Column?.Tag, ColumnTag.JobPurchaseOrders))
  354. {
  355. ShowDetailGrid<PurchaseOrderItem>(
  356. ColumnTag.GeneralPurchaseOrders.ToString(),
  357. x => x.Product.ID,
  358. productid,
  359. x => x.Style.ID,
  360. styleid,
  361. x=>x.Dimensions.UnitSize,
  362. unitsize,
  363. x=>x.Job.ID,
  364. new Filter<PurchaseOrderItem>(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue),
  365. null
  366. );
  367. }
  368. }
  369. #endregion
  370. #region Refresh
  371. private bool HasStyle()
  372. {
  373. return DataColumns().ColumnNames().Any(x => x.StartsWith("Style.") && !x.Equals("Style.ID"));
  374. }
  375. private Tuple<Guid,Guid?,String>[] GetKeys(IEnumerable<CoreRow> rows, Columns<ProductInstance> columns, bool hasstyle)
  376. {
  377. int productcol = columns.IndexOf(x => x.Product.ID);
  378. int stylecol = hasstyle ? columns.IndexOf(x => x.Style.ID) : -1;
  379. int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize);
  380. var result = rows.Select(r => new Tuple<Guid, Guid?, String>(
  381. (Guid)(r.Values[productcol] ?? Guid.Empty),
  382. (stylecol != -1) ? (Guid)(r.Values[stylecol] ?? Guid.Empty) : null,
  383. (String)(r.Values[unitcol] ?? ""))
  384. ).Distinct().ToArray();
  385. return result;
  386. }
  387. private CoreRow[] GetRows<TSource>(IEnumerable<CoreRow> rows, Columns<TSource> columns, Guid productid, Guid? styleid, string unitsize, Guid[] jobids) where TSource : IJobMaterial
  388. {
  389. int productcol = columns.IndexOf(x => x.Product.ID);
  390. int stylecol = styleid.HasValue ? columns.IndexOf(x => x.Style.ID) : -1;
  391. int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize);
  392. int jobcol = columns.IndexOf(x => x.Job.ID);
  393. var subset = rows
  394. .Where(r=>
  395. Guid.Equals(r.Values[productcol], productid)
  396. && (!styleid.HasValue || Guid.Equals(r.Values[stylecol], styleid))
  397. && String.Equals(r.Values[unitcol], unitsize)
  398. && jobids.Any(x=>Equals(x,r.Values[jobcol]))
  399. );
  400. return subset.ToArray();
  401. }
  402. private double Aggregate<TSource>(IEnumerable<CoreRow> rows, Columns<TSource> columns, bool hasstyle, bool hasjob, Expression<Func<TSource, object>> source, CoreRow? target = null, Expression<Func<StockSummary, object>>? aggregate = null)
  403. {
  404. int srcol = columns.IndexOf(source);
  405. if (srcol == -1)
  406. return 0.00;
  407. var total = rows.Aggregate(0d, (value, row) => value + (double)(row.Values[srcol] ?? 0.0d));
  408. // int productcol = columns.IndexOf(x => x.Product.ID);
  409. // int stylecol = hasstyle ? columns.IndexOf(x => x.Style.ID) : -1;
  410. // int jobcol = hasjob ? columns.IndexOf(x => x.Job.ID) : -1;
  411. // int unitcol = columns.IndexOf(x => x.Dimensions.UnitSize);
  412. //
  413. // var tuples = rows.Select(r => new Tuple<Guid, Guid?, Guid?, String, double>(
  414. // (Guid)(r.Values[productcol] ?? Guid.Empty),
  415. // (hasstyle ? (Guid)(r.Values[stylecol] ?? Guid.Empty) : null),
  416. // (hasjob ? (Guid)(r.Values[jobcol] ?? Guid.Empty) : null),
  417. // (String)(r.Values[unitcol] ?? ""),
  418. // (double)(r.Values[aggcol] ?? 0.0d))
  419. // ).ToArray();
  420. //
  421. // var total = tuples.Aggregate(0d, (value, tuple) => value + tuple.Item5);
  422. if(aggregate is not null)
  423. {
  424. target?.Set(aggregate, total);
  425. }
  426. return total;
  427. }
  428. private class StockSummaryJobInfo
  429. {
  430. public double BOM { get; set; }
  431. public double Stock { get; set; }
  432. public double PO { get; set; }
  433. public double Required => Math.Max(BOM - (Stock + PO), 0.0F);
  434. }
  435. private class StockSummaryInfo
  436. {
  437. public double MinStock { get; set; }
  438. public double GenStock { get; set; }
  439. public double GenPO { get; set; }
  440. public double JobBOM { get; set; }
  441. public double JobStock { get; set; }
  442. public double JobPO { get; set; }
  443. public Dictionary<Guid, StockSummaryJobInfo> JobInfo { get; private init; } = [];
  444. public void AddJobBOM(Guid jobID, double quantity)
  445. {
  446. var item = JobInfo.GetValueOrAdd(jobID);
  447. item.BOM += quantity;
  448. }
  449. public void AddJobPO(Guid jobID, double quantity)
  450. {
  451. var item = JobInfo.GetValueOrAdd(jobID);
  452. item.PO += quantity;
  453. }
  454. public void AddJobStock(Guid jobID, double quantity)
  455. {
  456. var item = JobInfo.GetValueOrAdd(jobID);
  457. item.Stock += quantity;
  458. }
  459. public double StockRequired => Math.Max(MinStock - (GenStock + GenPO), 0.0F);
  460. public double Required => Math.Max((MinStock + JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
  461. public double Optimised => Math.Max(Math.Max(MinStock, JobBOM) - (GenStock + GenPO + JobStock + JobPO), 0.0F);
  462. }
  463. private Dictionary<Guid, StockSummaryInfo> _summaryinfo = new Dictionary<Guid, StockSummaryInfo>();
  464. protected override void Reload(Filters<ProductInstance> criteria, Columns<ProductInstance> columns, ref SortOrder<ProductInstance>? sort,
  465. Action<CoreTable?, Exception?> action)
  466. {
  467. var query = new MultiQuery();
  468. query.Add<ProductInstance>(
  469. GroupIDs.Length == 0
  470. ? new Filter<ProductInstance>().None()
  471. : new Filter<ProductInstance>(pi=>pi.Product.Group.ID).InList(GroupIDs),
  472. columns,
  473. new SortOrder<ProductInstance>(x=>x.Product.Code)
  474. );
  475. query.Add<StockHolding>(
  476. GroupIDs.Length == 0
  477. ? new Filter<StockHolding>().None()
  478. : new Filter<StockHolding>(x=>x.Product.Group.ID).InList(GroupIDs)
  479. .And(new Filter<StockHolding>(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)),
  480. new Columns<StockHolding>(x=>x.Product.ID)
  481. .Add(x=>x.Job.ID)
  482. .Add(x=>x.Style.ID)
  483. .Add(x=>x.Dimensions.Unit.ID)
  484. .Add(x=>x.Dimensions.UnitSize)
  485. .Add(x=>x.Units),
  486. null
  487. );
  488. query.Add<PurchaseOrderItem>(
  489. GroupIDs.Length == 0
  490. ? new Filter<PurchaseOrderItem>().None()
  491. : new Filter<PurchaseOrderItem>(x=>x.Product.Group.ID).InList(GroupIDs)
  492. .And(x=>x.ReceivedDate).IsEqualTo(DateTime.MinValue)
  493. .And(new Filter<PurchaseOrderItem>(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)),
  494. new Columns<PurchaseOrderItem>(x=>x.Product.ID)
  495. .Add(x=>x.Job.ID)
  496. .Add(x=>x.Style.ID)
  497. .Add(x=>x.Dimensions.Unit.ID)
  498. .Add(x=>x.Dimensions.UnitSize)
  499. .Add(x=>x.Qty),
  500. null
  501. );
  502. query.Add<JobBillOfMaterialsItem>(
  503. GroupIDs.Length == 0
  504. ? new Filter<JobBillOfMaterialsItem>().None()
  505. : new Filter<JobBillOfMaterialsItem>(x=>x.Product.Group.ID).InList(GroupIDs)
  506. .And(new Filter<JobBillOfMaterialsItem>(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)),
  507. new Columns<JobBillOfMaterialsItem>(x=>x.Product.ID)
  508. .Add(x=>x.Job.ID)
  509. .Add(x=>x.Style.ID)
  510. .Add(x=>x.Dimensions.Unit.ID)
  511. .Add(x=>x.Dimensions.UnitSize)
  512. .Add(x=>x.Quantity),
  513. null
  514. );
  515. query.Add<StockMovement>(
  516. GroupIDs.Length == 0
  517. ? new Filter<StockMovement>().None()
  518. : new Filter<StockMovement>(x=>x.Product.Group.ID).InList(GroupIDs)
  519. .And(x=>x.Type).IsEqualTo(StockMovementType.Issue)
  520. .And(new Filter<StockMovement>(x=>x.Job.ID).InList(JobIDs).Or(x=>x.Job.ID).IsEqualTo(Guid.Empty)),
  521. new Columns<StockMovement>(x=>x.Product.ID)
  522. .Add(x=>x.Job.ID)
  523. .Add(x=>x.Style.ID)
  524. .Add(x=>x.Dimensions.Unit.ID)
  525. .Add(x=>x.Dimensions.UnitSize)
  526. .Add(x=>x.Units),
  527. null
  528. );
  529. query.Add<SupplierProduct>(
  530. GroupIDs.Length == 0
  531. ? new Filter<SupplierProduct>().None()
  532. : new Filter<SupplierProduct>(x => x.Product.Group.ID).InList(GroupIDs)
  533. .And(new Filter<SupplierProduct>(x => x.Job.ID).InList(JobIDs).Or(x => x.Job.ID).IsEqualTo(Guid.Empty)),
  534. new Columns<SupplierProduct>(x=>x.Product.ID)
  535. .Add(x=>x.Job.ID)
  536. .Add(x=>x.Style.ID)
  537. .Add(x=>x.Dimensions.Unit.ID)
  538. .Add(x=>x.Dimensions.UnitSize)
  539. .Add(x=>x.SupplierLink.ID)
  540. .Add(x=>x.CostPrice)
  541. .Add(x=>x.Discount)
  542. .Add(x=>x.TaxCode.ID),
  543. null
  544. );
  545. query.Query(q =>
  546. {
  547. _stockHoldings = query.Get<StockHolding>();
  548. _poItems = query.Get<PurchaseOrderItem>();
  549. _jobBOMs = query.Get<JobBillOfMaterialsItem>();
  550. _stockMovements = query.Get<StockMovement>();
  551. _supplierProducts = query.Get<SupplierProduct>();
  552. var products = query.Get<ProductInstance>();
  553. _summaryinfo.Clear();
  554. var _productInstanceCols = new Columns<ProductInstance>(products.Columns);
  555. var _idCol = _productInstanceCols.IndexOf(x => x.ID);
  556. var _productIDCol = _productInstanceCols.IndexOf(x => x.Product.ID);
  557. var _styleIDCol = _productInstanceCols.IndexOf(x => x.Style.ID);
  558. var _unitSizeCol = _productInstanceCols.IndexOf(x => x.Dimensions.UnitSize);
  559. var _stockHoldingCols = new Columns<StockHolding>(_stockHoldings.Columns);
  560. var _poItemCols = new Columns<PurchaseOrderItem>(_poItems.Columns);
  561. var _jobBOMColumns = new Columns<JobBillOfMaterialsItem>(_jobBOMs.Columns);
  562. var _stockMovementCols = new Columns<StockMovement>(_stockMovements.Columns);
  563. foreach (var row in products.Rows)
  564. {
  565. var _id = row.Values[_idCol] as Guid? ?? Guid.Empty;
  566. var _productid = row.Values[_productIDCol] as Guid? ?? Guid.Empty;
  567. var _styleid = row.Values[_styleIDCol] as Guid? ?? Guid.Empty;
  568. var _unitsize = row.Values[_unitSizeCol] as string ?? "";
  569. var info = new StockSummaryInfo();
  570. info.MinStock = row.Get<ProductInstance, double>(x => x.MinimumStockLevel);
  571. var genstockrows = GetRows(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, [Guid.Empty]);
  572. info.GenStock = Aggregate(genstockrows, _stockHoldingCols, true, true, x=>x.Units);
  573. var genporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, [Guid.Empty]);
  574. info.GenPO = Aggregate(genporows, _poItemCols, true, true, x=>x.Qty);
  575. // Job BOMs
  576. {
  577. var bomrows = GetRows(_jobBOMs.Rows, _jobBOMColumns, _productid, _styleid, _unitsize, JobIDs);
  578. var bom = Aggregate(bomrows, _jobBOMColumns, true, true, x => x.Quantity);
  579. var mvmtrows = GetRows(_stockMovements.Rows, _stockMovementCols, _productid, _styleid, _unitsize, JobIDs);
  580. var mvmts = Aggregate(mvmtrows, _stockMovementCols, true, true, x => x.Units);
  581. info.JobBOM = bom - mvmts;
  582. var bomJobCol = _jobBOMColumns.IndexOf(x => x.Job.ID);
  583. var bomQtyCol = _jobBOMColumns.IndexOf(x => x.Quantity);
  584. foreach(var jobBOMRow in bomrows)
  585. {
  586. info.AddJobBOM(jobBOMRow.Get<Guid>(bomJobCol), jobBOMRow.Get<double>(bomQtyCol));
  587. }
  588. var mvtJobCol = _stockMovementCols.IndexOf(x => x.Job.ID);
  589. var mvtQtyCol = _stockMovementCols.IndexOf(x => x.Qty);
  590. foreach(var mvtRow in mvmtrows)
  591. {
  592. info.AddJobBOM(mvtRow.Get<Guid>(mvtJobCol), -mvtRow.Get<double>(mvtQtyCol));
  593. }
  594. }
  595. // Job Stock
  596. {
  597. var jobstockrows = GetRows(_stockHoldings.Rows, _stockHoldingCols, _productid, _styleid, _unitsize, JobIDs);
  598. info.JobStock = Aggregate(jobstockrows, _stockHoldingCols, true, true, x=>x.Units);
  599. var jobCol = _stockHoldingCols.IndexOf(x => x.Job.ID);
  600. var qtyCol = _stockHoldingCols.IndexOf(x => x.Units);
  601. foreach(var jobStockRow in jobstockrows)
  602. {
  603. info.AddJobStock(jobStockRow.Get<Guid>(jobCol), jobStockRow.Get<double>(qtyCol));
  604. }
  605. }
  606. // Job PO
  607. {
  608. var jobporows = GetRows(_poItems.Rows, _poItemCols, _productid, _styleid, _unitsize, JobIDs);
  609. info.JobPO = Aggregate(jobporows, _poItemCols, true, true, x => x.Qty);
  610. var jobCol = _poItemCols.IndexOf(x => x.Job.ID);
  611. var qtyCol = _poItemCols.IndexOf(x => x.Qty);
  612. foreach(var jobPORow in jobporows)
  613. {
  614. info.AddJobPO(jobPORow.Get<Guid>(jobCol), jobPORow.Get<double>(qtyCol));
  615. }
  616. }
  617. _summaryinfo[_id] = info;
  618. }
  619. // Process the tables here
  620. action.Invoke(query.Get<ProductInstance>(), null);
  621. });
  622. }
  623. protected override bool FilterRecord(CoreRow row)
  624. {
  625. bool result = base.FilterRecord(row);
  626. if (_summaryinfo.TryGetValue(row.Get<ProductInstance, Guid>(x => x.ID), out StockSummaryInfo? info))
  627. {
  628. if (RequiredOnly)
  629. {
  630. result = result && Optimise
  631. ? !info.Optimised.IsEffectivelyEqual(0)
  632. : !info.Required.IsEffectivelyEqual(0);
  633. }
  634. if (!AllStock)
  635. result = result && _uicomponent?.CheckSuppliers(row) == true;
  636. }
  637. return result;
  638. }
  639. #endregion
  640. #region Ordering
  641. private bool SelectForOrder_Click(CoreRow? row)
  642. {
  643. if (row is null)
  644. {
  645. var menu = new ContextMenu();
  646. menu.AddItem("Deselect all", null, () =>
  647. {
  648. SelectedForOrder.Clear();
  649. InvalidateGrid();
  650. OrderButton.IsEnabled = false;
  651. });
  652. menu.IsOpen = true;
  653. return false;
  654. }
  655. else
  656. {
  657. var id = row.Get<ProductInstance, Guid>(x => x.ID);
  658. if (!SelectedForOrder.Remove(id))
  659. {
  660. SelectedForOrder.Add(id);
  661. }
  662. OrderButton.IsEnabled = SelectedForOrder.Count > 0;
  663. InvalidateRow(row);
  664. return false;
  665. }
  666. }
  667. private BitmapImage? SelectForOrder_Image(CoreRow? row)
  668. {
  669. if(row is null)
  670. {
  671. return _cart;
  672. }
  673. else if(SelectedForOrder.Contains(row.Get<ProductInstance, Guid>(x => x.ID)))
  674. {
  675. return _tick;
  676. }
  677. else
  678. {
  679. return null;
  680. }
  681. }
  682. private bool OrderStock_Click(Button button, CoreRow[] rows)
  683. {
  684. if(rows.Length == 0)
  685. {
  686. return false;
  687. }
  688. rows = Data.Rows.Where(x => SelectedForOrder.Contains(x.Get<ProductInstance, Guid>(x => x.ID))).ToArray();
  689. var items = new List<StockSummaryOrderingItem>();
  690. foreach(var instance in rows.ToObjects<ProductInstance>())
  691. {
  692. var info = _summaryinfo.GetValueOrDefault(instance.ID);
  693. var item = new StockSummaryOrderingItem();
  694. item.Product.CopyFrom(instance.Product);
  695. item.Style.CopyFrom(instance.Style);
  696. item.Dimensions.CopyFrom(instance.Dimensions);
  697. item.RequiredQuantity = (Optimise ? info?.Optimised : info?.Required) ?? default;
  698. if(info is not null)
  699. {
  700. item.SetJobRequiredQuantity(Guid.Empty, info.StockRequired);
  701. foreach(var (id, jobInfo) in info.JobInfo)
  702. {
  703. item.SetJobRequiredQuantity(id, jobInfo.Required);
  704. }
  705. }
  706. else
  707. {
  708. item.SetJobRequiredQuantity(Guid.Empty, 0.0);
  709. }
  710. items.Add(item);
  711. }
  712. var window = new StockSummaryOrderScreen(items);
  713. if(window.ShowDialog() != true)
  714. {
  715. return false;
  716. }
  717. var orders = new List<Tuple<PurchaseOrder, List<PurchaseOrderItem>>>();
  718. foreach(var perSupplier in window.Results.GroupBy(x => x.Supplier.ID))
  719. {
  720. var order = new PurchaseOrder();
  721. order.Description = "Purchase Order created from Stock Forecast Screen";
  722. order.RaisedBy.ID = App.EmployeeID;
  723. LookupFactory.DoLookup<PurchaseOrder, Supplier, SupplierLink>(order, x => x.SupplierLink, perSupplier.Key);
  724. var orderItems = new List<PurchaseOrderItem>();
  725. var results = perSupplier.ToArray();
  726. foreach(var item in results)
  727. {
  728. var orderItem = new PurchaseOrderItem();
  729. orderItem.Product.ID = item.Item.Product.ID;
  730. orderItem.Style.ID = item.Item.Style.ID;
  731. orderItem.Job.ID = item.Job?.ID ?? Guid.Empty;
  732. orderItems.Add(orderItem);
  733. }
  734. LookupFactory.DoLookups<PurchaseOrderItem, Product, ProductLink>(
  735. orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Product.ID)),
  736. x => x.Product);
  737. LookupFactory.DoLookups<PurchaseOrderItem, ProductStyle, ProductStyleLink>(
  738. orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Style.ID)),
  739. x => x.Style);
  740. LookupFactory.DoLookups<PurchaseOrderItem, Job, JobLink>(
  741. orderItems.Select(x => new Tuple<PurchaseOrderItem, Guid>(x, x.Job.ID)),
  742. x => x.Job);
  743. LookupFactory.DoLookups<PurchaseOrderItem, TaxCode, TaxCodeLink>(
  744. orderItems.WithIndex().Select(x => new Tuple<PurchaseOrderItem, Guid>(x.Value, results[x.Key].SupplierProduct.TaxCode.ID)),
  745. x => x.TaxCode);
  746. foreach(var (i, item) in results.WithIndex())
  747. {
  748. var orderItem = orderItems[i];
  749. orderItem.Dimensions.CopyFrom(item.Item.Dimensions);
  750. orderItem.Qty = item.Quantity;
  751. orderItem.Cost = item.SupplierProduct.CostPrice;
  752. }
  753. orders.Add(new(order, orderItems));
  754. }
  755. Client.Save(orders.Select(x => x.Item1), "Created from Stock Forecast screen");
  756. foreach(var (order, orderItems) in orders)
  757. {
  758. foreach(var item in orderItems)
  759. {
  760. item.PurchaseOrderLink.ID = order.ID;
  761. }
  762. }
  763. Client.Save(orders.SelectMany(x => x.Item2), "Created from Stock Forecast screen");
  764. SelectedForOrder.Clear();
  765. OrderButton.IsEnabled = false;
  766. return true;
  767. }
  768. #endregion
  769. #region IDataModelSource
  770. public event DataModelUpdateEvent? OnUpdateDataModel;
  771. public string SectionName => "Stock Summary";
  772. public bool Optimise { get; set; }
  773. public bool AllStock { get; set; }
  774. public bool RequiredOnly { get; set; }
  775. public DataModel DataModel(Selection selection)
  776. {
  777. return new AutoDataModel<StockSummary>(null);
  778. }
  779. #endregion
  780. }