StockForecastOrderingGrid.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  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 NPOI.SS.Formula.Functions;
  8. using Syncfusion.Data;
  9. using Syncfusion.Data.Extensions;
  10. using Syncfusion.UI.Xaml.Grid;
  11. using Syncfusion.Windows.Shared;
  12. using System;
  13. using System.Collections;
  14. using System.Collections.Generic;
  15. using System.ComponentModel;
  16. using System.Data;
  17. using System.Linq;
  18. using System.Text;
  19. using System.Threading.Tasks;
  20. using System.Windows;
  21. using System.Windows.Controls;
  22. using System.Windows.Media;
  23. using System.Windows.Media.Imaging;
  24. using Columns = InABox.Core.Columns;
  25. namespace PRSDesktop.Panels.StockForecast.OrderScreen;
  26. public class StockForecastOrderData(ProductLink product, ProductStyleLink style, StockDimensions dimensions)
  27. {
  28. public ProductLink Product { get; set; } = product;
  29. public ProductStyleLink Style { get; set; } = style;
  30. public StockDimensions Dimensions { get; set; } = dimensions;
  31. public double RequiredQuantity { get; set; }
  32. private Dictionary<Guid, double> JobRequiredQuantities { get; set; } = [];
  33. public Dictionary<Guid, double> GetJobRequiredQuantities()
  34. {
  35. return JobRequiredQuantities;
  36. }
  37. public void SetJobRequiredQuantity(Guid jobID, double requiredQty)
  38. {
  39. JobRequiredQuantities[jobID] = requiredQty;
  40. }
  41. }
  42. public enum StockForecastOrderingType
  43. {
  44. StockOrder,
  45. JobOrder
  46. }
  47. public class StockForecastOrderingItemQuantity
  48. {
  49. public event Action? Changed;
  50. private double _total;
  51. public double Total
  52. {
  53. get => _total;
  54. set
  55. {
  56. _total = value;
  57. Changed?.Invoke();
  58. }
  59. }
  60. private SupplierProduct? _supplierProduct;
  61. /// <summary>
  62. /// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column.
  63. /// </summary>
  64. public SupplierProduct? SupplierProduct
  65. {
  66. get => _supplierProduct;
  67. set
  68. {
  69. _supplierProduct = value;
  70. Changed?.Invoke();
  71. }
  72. }
  73. public void DoChanged()
  74. {
  75. Changed?.Invoke();
  76. }
  77. }
  78. public class StockForecastOrderingItem : BaseObject
  79. {
  80. [EditorSequence(1)]
  81. public ProductLink Product { get; set; }
  82. [EditorSequence(2)]
  83. public ProductStyleLink Style { get; set; }
  84. [EditorSequence(3)]
  85. public StockDimensions Dimensions { get; set; }
  86. [EditorSequence(4)]
  87. public JobLink Job { get; set; }
  88. [EditorSequence(5)]
  89. [DoubleEditor]
  90. public double RequiredQuantity { get; set; }
  91. [EditorSequence(6)]
  92. [EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
  93. public SupplierProductOrderStrategy OrderStrategy { get; set; }
  94. public StockForecastOrderingItemQuantity[] Quantities = [];
  95. public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i];
  96. public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  97. ? Quantities.Sum(x => x.Total)
  98. : Quantities.Sum(x => x.Total);
  99. public void SetQuantities(StockForecastOrderingItemQuantity[] quantities)
  100. {
  101. Quantities = quantities;
  102. }
  103. }
  104. public class StockForecastOrderingResult
  105. {
  106. public SupplierLink Supplier { get; set; }
  107. public JobLink? Job { get; set; }
  108. public StockForecastOrderingItem Item { get; set; }
  109. public SupplierProduct SupplierProduct { get; set; }
  110. public double Quantity { get; set; }
  111. public StockForecastOrderingResult(SupplierLink supplier, JobLink? job, StockForecastOrderingItem item, double quantity, SupplierProduct supplierProduct)
  112. {
  113. Supplier = supplier;
  114. Job = job;
  115. Item = item;
  116. Quantity = quantity;
  117. SupplierProduct = supplierProduct;
  118. }
  119. }
  120. public enum StockForecastOrderingStrategy
  121. {
  122. PerProduct,
  123. Exact,
  124. RoundUp,
  125. LowestUnitPrice,
  126. LowestOverallPrice,
  127. LowestOverstock
  128. }
  129. public class StockForecastOrderingGrid : DynamicItemsListGrid<StockForecastOrderingItem>, ISpecificGrid
  130. {
  131. private List<SupplierProduct> SupplierProducts = [];
  132. private SupplierLink[] Suppliers = [];
  133. public IList<StockForecastOrderData> OrderData { get; set; }
  134. public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType));
  135. private DynamicActionColumn[] SupplierProductColumns = [];
  136. private DynamicActionColumn[] QuantityColumns = [];
  137. private DynamicActionColumn[] CostColumns = [];
  138. private readonly Dictionary<Guid, Job> JobDetails = [];
  139. private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
  140. private StockForecastOrderingType _orderType = StockForecastOrderingType.JobOrder;
  141. public StockForecastOrderingType OrderType
  142. {
  143. get => _orderType;
  144. set
  145. {
  146. if(_orderType != value)
  147. {
  148. _orderType = value;
  149. CalculateQuantities(true);
  150. UIComponent.UpdateOrderType(OrderType);
  151. Refresh(true, true);
  152. }
  153. }
  154. }
  155. private StockForecastOrderingStrategy orderStrategy;
  156. public StockForecastOrderingStrategy OrderStrategy
  157. {
  158. get => orderStrategy;
  159. set
  160. {
  161. orderStrategy = value;
  162. foreach(var item in Items)
  163. {
  164. item.OrderStrategy = value switch
  165. {
  166. StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact,
  167. StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy.LowestOverallPrice,
  168. StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice,
  169. StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock,
  170. StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp,
  171. StockForecastOrderingStrategy.PerProduct or _ => item.Product.OrderStrategy
  172. };
  173. }
  174. CalculateQuantities(false);
  175. Refresh(false, true);
  176. }
  177. }
  178. public IEnumerable<StockForecastOrderingResult> Results
  179. {
  180. get
  181. {
  182. for(int i = 0; i < Suppliers.Length; ++i)
  183. {
  184. var supplier = Suppliers[i];
  185. foreach(var item in Items)
  186. {
  187. var qty = item.GetQuantity(i);
  188. if (qty.SupplierProduct is null)
  189. {
  190. continue;
  191. }
  192. if(qty.Total > 0)
  193. {
  194. if(OrderType == StockForecastOrderingType.StockOrder && qty.Total > 0)
  195. {
  196. yield return new(supplier, null, item, qty.Total, qty.SupplierProduct);
  197. }
  198. else
  199. {
  200. yield return new(supplier, item.Job, item, qty.Total, qty.SupplierProduct);
  201. }
  202. }
  203. }
  204. }
  205. }
  206. }
  207. public StockForecastOrderingGrid()
  208. {
  209. HiddenColumns.Add(x => x.Product.Image.ID);
  210. }
  211. #region UI Component
  212. private Component? _uiComponent;
  213. private Component UIComponent
  214. {
  215. get
  216. {
  217. _uiComponent ??= new Component(this);
  218. return _uiComponent;
  219. }
  220. }
  221. protected override IDynamicGridUIComponent<StockForecastOrderingItem> CreateUIComponent()
  222. {
  223. return UIComponent;
  224. }
  225. private class Component : DynamicGridGridUIComponent<StockForecastOrderingItem>
  226. {
  227. private StockForecastOrderingGrid Grid;
  228. public Component(StockForecastOrderingGrid grid)
  229. {
  230. Parent = grid;
  231. Grid = grid;
  232. UpdateOrderType(grid.OrderType);
  233. }
  234. public void UpdateOrderType(StockForecastOrderingType type)
  235. {
  236. DataGrid.FrozenColumnCount = type == StockForecastOrderingType.StockOrder ? 8 : 9;
  237. }
  238. protected override Brush? GetCellSelectionBackgroundBrush()
  239. {
  240. return null;
  241. }
  242. protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
  243. {
  244. var item = Grid.LoadItem(row);
  245. if(column is DynamicActionColumn ac)
  246. {
  247. var qIdx = Grid.QuantityColumns.IndexOf(ac);
  248. var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
  249. if(idx != -1)
  250. {
  251. var supplierProduct = item.GetQuantity(idx).SupplierProduct;
  252. if(supplierProduct is null)
  253. {
  254. return new SolidColorBrush(Colors.Gainsboro);
  255. }
  256. //if(item.GetTotalQuantity(Grid.OrderType) < item.RequiredQuantity)
  257. //{
  258. // return new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 };
  259. //}
  260. //else
  261. //{
  262. // return new SolidColorBrush(Colors.LightGreen) { Opacity = 0.5 };
  263. //}
  264. }
  265. }
  266. return base.GetCellBackground(row, column);
  267. }
  268. }
  269. #endregion
  270. private bool _observing = true;
  271. private void SetObserving(bool observing)
  272. {
  273. _observing = observing;
  274. }
  275. protected override void Changed()
  276. {
  277. if (_observing)
  278. {
  279. base.Changed();
  280. }
  281. }
  282. protected override void DoReconfigure(DynamicGridOptions options)
  283. {
  284. options.Clear();
  285. options.FilterRows = true;
  286. }
  287. private bool _loadedData = false;
  288. private void LoadData()
  289. {
  290. var supplierColumns = Columns.None<SupplierProduct>().Add(x => x.ID)
  291. .Add(x => x.SupplierLink.ID)
  292. .Add(x => x.Product.ID)
  293. .Add(x => x.Style.ID)
  294. .Add(x => x.Style.Code)
  295. .Add(x => x.ForeignCurrencyPrice)
  296. .Add(x => x.CostPrice)
  297. .AddDimensionsColumns(x => x.Dimensions)
  298. .Add(x => x.SupplierLink.Code);
  299. SupplierProducts = Client.Query(
  300. new Filter<SupplierProduct>(x => x.Product.ID).InList(OrderData.Select(x => x.Product.ID).ToArray())
  301. .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
  302. supplierColumns,
  303. new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
  304. .ToList<SupplierProduct>();
  305. Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
  306. LoadJobData(OrderData.SelectMany(x => x.GetJobRequiredQuantities().Keys).Distinct().Where(x => x != Guid.Empty));
  307. CalculateQuantities(true);
  308. _loadedData = true;
  309. }
  310. private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx)
  311. {
  312. var qty = new StockForecastOrderingItemQuantity();
  313. qty.Changed += () =>
  314. {
  315. if (!_observing) return;
  316. var row = Data.Rows[itemIdx];
  317. InvalidateRow(row);
  318. DoChanged();
  319. };
  320. return qty;
  321. }
  322. private void CalculateSupplierProduct(StockForecastOrderingItem item)
  323. {
  324. var selectedSupplierProducts = new List<SupplierProduct>();
  325. for(int i = 0; i < Suppliers.Length; ++i)
  326. {
  327. var supplierProduct = SelectSupplierProduct(SupplierProducts.Where(x => x.Product.ID == item.Product.ID && x.Style.ID == item.Style.ID && x.SupplierLink.ID == Suppliers[i].ID), item);
  328. var qty = item.GetQuantity(i);
  329. qty.SupplierProduct = supplierProduct;
  330. qty.Total = 0;
  331. if(supplierProduct is not null)
  332. {
  333. selectedSupplierProducts.Add(supplierProduct);
  334. }
  335. }
  336. var selectedSupplierProduct = SelectSupplierProduct(selectedSupplierProducts, item);
  337. if(selectedSupplierProduct is not null)
  338. {
  339. var supplierIdx = Suppliers.WithIndex()
  340. .FirstOrDefault(x => x.Value.ID == selectedSupplierProduct.SupplierLink.ID, new KeyValuePair<int, SupplierLink>(-1, null)).Key;
  341. if(supplierIdx != -1)
  342. {
  343. item.GetQuantity(supplierIdx).Total = GetRequiredQuantity(item, selectedSupplierProduct);
  344. }
  345. }
  346. }
  347. private void CalculateQuantities(bool recreateItems)
  348. {
  349. SetObserving(false);
  350. if (recreateItems)
  351. {
  352. Items.Clear();
  353. foreach(var dataItem in OrderData)
  354. {
  355. if(OrderType == StockForecastOrderingType.StockOrder)
  356. {
  357. var item = new StockForecastOrderingItem();
  358. item.Product.CopyFrom(dataItem.Product);
  359. item.Style.CopyFrom(dataItem.Style);
  360. item.Dimensions.CopyFrom(dataItem.Dimensions);
  361. item.RequiredQuantity = dataItem.RequiredQuantity;
  362. item.OrderStrategy = item.Product.OrderStrategy;
  363. Items.Add(item);
  364. }
  365. else
  366. {
  367. foreach(var (id, q) in dataItem.GetJobRequiredQuantities())
  368. {
  369. var item = new StockForecastOrderingItem();
  370. item.Product.CopyFrom(dataItem.Product);
  371. item.Style.CopyFrom(dataItem.Style);
  372. item.Dimensions.CopyFrom(dataItem.Dimensions);
  373. item.Job.ID = id;
  374. if(id != Guid.Empty)
  375. {
  376. item.Job.CopyFrom(JobDetails[id]);
  377. }
  378. item.RequiredQuantity = q;
  379. item.OrderStrategy = item.Product.OrderStrategy;
  380. Items.Add(item);
  381. }
  382. }
  383. }
  384. }
  385. foreach(var (itemIdx, item) in Items.WithIndex())
  386. {
  387. var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
  388. for(int i = 0; i < Suppliers.Length; ++i)
  389. {
  390. quantities[i] = CreateQuantity(itemIdx);
  391. }
  392. item.SetQuantities(quantities);
  393. }
  394. foreach(var item in Items)
  395. {
  396. CalculateSupplierProduct(item);
  397. }
  398. SetObserving(true);
  399. DoChanged();
  400. }
  401. private SupplierProduct? SelectSupplierProduct(IEnumerable<SupplierProduct> supplierProducts, StockForecastOrderingItem item)
  402. {
  403. switch (item.OrderStrategy)
  404. {
  405. case SupplierProductOrderStrategy.Exact:
  406. case SupplierProductOrderStrategy.RoundUp:
  407. // First, find the cheapest in the right style and dimensions.
  408. return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions) && x.Style.ID == item.Style.ID).MinBy(x => x.CostPrice)
  409. // Otherwise, find the cheapest in the right dimensions.
  410. ?? supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions)).MinBy(x => x.CostPrice);
  411. default:
  412. return null;
  413. }
  414. }
  415. private double GetRequiredQuantity(StockForecastOrderingItem item, SupplierProduct supplierProduct)
  416. {
  417. switch (item.OrderStrategy)
  418. {
  419. case SupplierProductOrderStrategy.Exact:
  420. return item.RequiredQuantity;
  421. case SupplierProductOrderStrategy.RoundUp:
  422. return Math.Ceiling(item.RequiredQuantity);
  423. default:
  424. return 0.0;
  425. }
  426. }
  427. private bool _loadedColumns = false;
  428. protected override DynamicGridColumns LoadColumns()
  429. {
  430. if (!_loadedData)
  431. {
  432. LoadData();
  433. }
  434. var columns = new DynamicGridColumns();
  435. columns.Add<StockForecastOrderingItem, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
  436. columns.Add<StockForecastOrderingItem, string>(x => x.Product.Name, 200, "Product Name", "", Alignment.MiddleLeft);
  437. columns.Add<StockForecastOrderingItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter);
  438. columns.Add<StockForecastOrderingItem, string>(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter);
  439. if(OrderType == StockForecastOrderingType.JobOrder)
  440. {
  441. columns.Add<StockForecastOrderingItem, string>(x => x.Job.JobNumber, 80, "Job No.", "", Alignment.MiddleCenter);
  442. }
  443. columns.Add<StockForecastOrderingItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
  444. if (!_loadedColumns)
  445. {
  446. ActionColumns.Clear();
  447. ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
  448. ActionColumns.Add(new DynamicImagePreviewColumn<StockForecastOrderingItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
  449. ActionColumns.Add(new DynamicTemplateColumn(row =>
  450. {
  451. var item = LoadItem(row);
  452. var box = new ComboBox();
  453. box.ItemsSource = Enum.GetValues<SupplierProductOrderStrategy>()
  454. .Select(x => new KeyValuePair<SupplierProductOrderStrategy, string>(x, CoreUtils.Neatify(x.ToString())));
  455. box.DisplayMemberPath = "Value";
  456. box.SelectedValuePath = "Key";
  457. box.SelectedValue = item.OrderStrategy;
  458. box.SelectionChanged += (o, e) =>
  459. {
  460. item.OrderStrategy = (SupplierProductOrderStrategy)box.SelectedValue;
  461. CalculateSupplierProduct(item);
  462. InvalidateRow(row);
  463. };
  464. box.Margin = new Thickness(2);
  465. box.VerticalContentAlignment = VerticalAlignment.Center;
  466. return box;
  467. })
  468. {
  469. HeaderText = "Order Strategy.",
  470. Width = 120
  471. });
  472. SupplierProductColumns = new DynamicActionColumn[Suppliers.Length];
  473. QuantityColumns = new DynamicActionColumn[Suppliers.Length];
  474. CostColumns = new DynamicActionColumn[Suppliers.Length];
  475. QuantityControls.Clear();
  476. for(int i = 0; i < Suppliers.Length; ++i)
  477. {
  478. InitialiseSupplierColumn(i);
  479. }
  480. ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
  481. _loadedColumns = true;
  482. }
  483. return columns;
  484. }
  485. private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
  486. {
  487. grid.OnCustomiseEditor += (sender, items, column, editor) =>
  488. {
  489. if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
  490. || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
  491. || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName)
  492. || new Column<SupplierProduct>(x => x.Job.ID).IsEqualTo(column.ColumnName)
  493. || new Column<SupplierProduct>(x => x.Dimensions).IsEqualTo(column.ColumnName))
  494. {
  495. editor.Editable = editor.Editable.Combine(Editable.Disabled);
  496. }
  497. };
  498. }
  499. private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
  500. {
  501. if (row is null) return;
  502. column.AddItem("New Supplier", null, row =>
  503. {
  504. if (row is null) return;
  505. var selection = new MultiSelectDialog<Supplier>(
  506. new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
  507. Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
  508. if (selection.ShowDialog() != true)
  509. {
  510. return;
  511. }
  512. var supplier = selection.Data().Rows.First().ToObject<Supplier>();
  513. var productInstance = LoadItem(row);
  514. var supplierProduct = new SupplierProduct();
  515. supplierProduct.Product.CopyFrom(productInstance.Product);
  516. supplierProduct.Style.CopyFrom(productInstance.Style);
  517. supplierProduct.Dimensions.CopyFrom(productInstance.Dimensions);
  518. supplierProduct.SupplierLink.CopyFrom(supplier);
  519. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  520. {
  521. SupplierProducts.Add(supplierProduct);
  522. var newSuppliers = new SupplierLink[Suppliers.Length + 1];
  523. var newIdx = Suppliers.Length;
  524. for (int i = 0; i < Suppliers.Length; i++)
  525. {
  526. newSuppliers[i] = Suppliers[i];
  527. }
  528. newSuppliers[newIdx] = supplierProduct.SupplierLink;
  529. foreach (var (itemIdx, item) in Items.WithIndex())
  530. {
  531. var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length];
  532. for (int i = 0; i < Suppliers.Length; ++i)
  533. {
  534. quantities[i] = item.GetQuantity(i);
  535. }
  536. var newQty = CreateQuantity(itemIdx);
  537. quantities[newIdx] = newQty;
  538. newQty.Total = 0;
  539. item.SetQuantities(quantities);
  540. }
  541. Suppliers = newSuppliers;
  542. Refresh(true, true);
  543. }
  544. });
  545. }
  546. private BitmapImage? Warning_Image(CoreRow? row)
  547. {
  548. if (row is null) return _warning;
  549. var item = LoadItem(row);
  550. if(item.GetTotalQuantity(OrderType) < item.RequiredQuantity)
  551. {
  552. return _warning;
  553. }
  554. else
  555. {
  556. return null;
  557. }
  558. }
  559. protected override void ConfigureColumnGroups()
  560. {
  561. for(int idx = 0; idx < Suppliers.Length; ++idx)
  562. {
  563. GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]);
  564. }
  565. }
  566. private void LoadJobData(IEnumerable<Guid> ids)
  567. {
  568. var neededIDs = ids.Where(x => !JobDetails.ContainsKey(x)).ToArray();
  569. if(neededIDs.Length > 0)
  570. {
  571. var details = Client.Query(
  572. new Filter<Job>(x => x.ID).InList(neededIDs),
  573. Columns.None<Job>().Add(x => x.ID)
  574. .Add(x => x.JobNumber)
  575. .Add(x => x.Name));
  576. foreach(var job in details.ToObjects<Job>())
  577. {
  578. JobDetails[job.ID] = job;
  579. }
  580. }
  581. }
  582. private class QuantityControl : ContentControl
  583. {
  584. private readonly StockForecastOrderingItem Item;
  585. private readonly int SupplierIndex;
  586. private readonly StockForecastOrderingGrid Parent;
  587. public QuantityControl(StockForecastOrderingGrid parent, StockForecastOrderingItem item, int supplierIndex, StockForecastOrderingType mode)
  588. {
  589. Parent = parent;
  590. Item = item;
  591. SupplierIndex = supplierIndex;
  592. UpdateControl(mode);
  593. }
  594. public void UpdateControl(StockForecastOrderingType mode)
  595. {
  596. // If no supplier product has been selected for this cell, we can't allow the user to select a quantity.
  597. if(Item.GetQuantity(SupplierIndex).SupplierProduct is null)
  598. {
  599. Content = null;
  600. return;
  601. }
  602. // Otherwise, simple quantity textbox editor.
  603. var editor = new DoubleTextBox
  604. {
  605. VerticalAlignment = VerticalAlignment.Stretch,
  606. HorizontalAlignment = HorizontalAlignment.Stretch,
  607. Background = new SolidColorBrush(Colors.LightYellow),
  608. BorderThickness = new Thickness(0.0),
  609. MinValue = 0.0,
  610. Value = Item.GetQuantity(SupplierIndex).Total
  611. };
  612. editor.ValueChanged += (o, e) =>
  613. {
  614. Item.GetQuantity(SupplierIndex).Total = editor.Value ?? default;
  615. };
  616. Content = editor;
  617. }
  618. }
  619. private List<QuantityControl> QuantityControls = [];
  620. private void InitialiseSupplierColumn(int idx)
  621. {
  622. var contextMenuFunc = (CoreRow[]? rows) =>
  623. {
  624. var row = rows?.FirstOrDefault();
  625. if (row is null) return null;
  626. var item = LoadItem(row);
  627. var supplierProduct = GetSupplierProduct(item, Suppliers[idx].ID);
  628. if (supplierProduct is not null)
  629. {
  630. return null;
  631. }
  632. var menu = new ContextMenu();
  633. menu.AddItem("Create Supplier Product", null, new Tuple<StockForecastOrderingItem, int>(item, idx), CreateSupplierProduct_Click);
  634. return menu;
  635. };
  636. var qtyColumn = new Tuple<DynamicActionColumn, QuantityControl?>(null!, null);
  637. SupplierProductColumns[idx] = new DynamicTemplateColumn(row =>
  638. {
  639. var instance = LoadItem(row);
  640. var comboBox = new ComboBox();
  641. comboBox.ItemsSource =
  642. SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID)
  643. .Select(x => new KeyValuePair<SupplierProduct?, string>(
  644. x,
  645. x.Style.ID != Guid.Empty ? $"{x.Dimensions.UnitSize}/{x.Style.Code}" : $"{x.Dimensions.UnitSize}"));
  646. comboBox.SelectedValuePath = "Key";
  647. comboBox.DisplayMemberPath = "Value";
  648. var qty = instance.GetQuantity(idx);
  649. comboBox.Bind(ComboBox.SelectedValueProperty, qty, x => x.SupplierProduct);
  650. comboBox.VerticalContentAlignment = VerticalAlignment.Center;
  651. comboBox.Margin = new Thickness(2);
  652. return comboBox;
  653. })
  654. {
  655. HeaderText = "Supplier Product.",
  656. Width = 80
  657. };
  658. QuantityColumns[idx] = new DynamicTemplateColumn(row =>
  659. {
  660. var instance = LoadItem(row);
  661. var control = new QuantityControl(this, instance, idx, OrderType);
  662. QuantityControls.Add(control);
  663. return control;
  664. })
  665. {
  666. HeaderText = "Qty.",
  667. Width = 80,
  668. ContextMenu = contextMenuFunc
  669. };
  670. CostColumns[idx] = new DynamicTextColumn(row =>
  671. {
  672. if(row is null)
  673. {
  674. return "Cost";
  675. }
  676. var instance = LoadItem(row);
  677. var qty = instance.GetQuantity(idx);//.Total;
  678. if(qty.SupplierProduct is not null)
  679. {
  680. return $"{qty.Total * qty.SupplierProduct.CostPrice:C2}";
  681. }
  682. else
  683. {
  684. return "";
  685. }
  686. })
  687. {
  688. HeaderText = "Cost",
  689. Width = 80,
  690. ContextMenu = contextMenuFunc,
  691. GetSummary = () =>
  692. {
  693. var summary = new GridSummaryColumn
  694. {
  695. Format = "{Sum:C2}",
  696. SummaryType = Syncfusion.Data.SummaryType.Custom,
  697. CustomAggregate = new CostAggregate(idx, this)
  698. };
  699. return summary;
  700. }
  701. };
  702. ActionColumns.Add(SupplierProductColumns[idx]);
  703. ActionColumns.Add(QuantityColumns[idx]);
  704. ActionColumns.Add(CostColumns[idx]);
  705. }
  706. private void CreateSupplierProduct_Click(Tuple<StockForecastOrderingItem, int> tuple)
  707. {
  708. var (item, supplierIdx) = tuple;
  709. var supplierProduct = new SupplierProduct();
  710. supplierProduct.Product.CopyFrom(item.Product);
  711. supplierProduct.Style.CopyFrom(item.Style);
  712. supplierProduct.Dimensions.CopyFrom(item.Dimensions);
  713. supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]);
  714. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  715. {
  716. SupplierProducts.Add(supplierProduct);
  717. InvalidateGrid();
  718. }
  719. }
  720. private static bool Matches(StockForecastOrderingItem item, SupplierProduct supplierProduct)
  721. {
  722. return item.Product.ID == supplierProduct.Product.ID
  723. && item.Style.ID == supplierProduct.Style.ID
  724. && item.Dimensions.Equals(supplierProduct.Dimensions);
  725. }
  726. private SupplierProduct? GetSupplierProduct(StockForecastOrderingItem item, Guid supplierID)
  727. {
  728. return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x));
  729. }
  730. private class CostAggregate : ISummaryAggregate
  731. {
  732. public double Sum { get; private set; }
  733. private int SupplierIndex;
  734. private StockForecastOrderingGrid Grid;
  735. public CostAggregate(int supplierIndex, StockForecastOrderingGrid grid)
  736. {
  737. SupplierIndex = supplierIndex;
  738. Grid = grid;
  739. }
  740. public Action<IEnumerable, string, PropertyDescriptor> CalculateAggregateFunc()
  741. {
  742. return AggregateFunc;
  743. }
  744. private void AggregateFunc(IEnumerable items, string property, PropertyDescriptor args)
  745. {
  746. if (items is IEnumerable<DataRowView> rows)
  747. {
  748. Sum = 0;
  749. foreach (var dataRow in rows)
  750. {
  751. var rowIdx = dataRow.Row.Table.Rows.IndexOf(dataRow.Row);
  752. var item = Grid.LoadItem(Grid.Data.Rows[rowIdx]);
  753. var qty = item.GetQuantity(SupplierIndex);
  754. if(qty.SupplierProduct is not null)
  755. {
  756. Sum += qty.Total * qty.SupplierProduct.CostPrice;
  757. }
  758. }
  759. }
  760. else
  761. {
  762. Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'.");
  763. }
  764. }
  765. }
  766. }