StockForecastOrderingGrid.cs 32 KB

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