StockForecastOrderingGrid.cs 30 KB

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