StockForecastOrderingGrid.cs 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  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 System;
  12. using System.Collections;
  13. using System.Collections.Generic;
  14. using System.ComponentModel;
  15. using System.Data;
  16. using System.Linq;
  17. using System.Text;
  18. using System.Threading.Tasks;
  19. using System.Windows;
  20. using System.Windows.Controls;
  21. using System.Windows.Media;
  22. using System.Windows.Media.Imaging;
  23. using Columns = InABox.Core.Columns;
  24. using PRSDesktop.Panels.StockForecast.OrderScreen;
  25. using PRSDimensionUtils;
  26. using Syncfusion.Windows.Shared;
  27. namespace PRSDesktop;
  28. public class StockForecastBreakupKey(Guid jobID, Guid requiID)
  29. {
  30. public Guid JobID { get; set; } = jobID;
  31. public Guid RequiID { get; set; } = requiID;
  32. public override bool Equals(object? obj)
  33. {
  34. return obj is StockForecastBreakupKey key && key.JobID == JobID && key.RequiID == RequiID;
  35. }
  36. public override int GetHashCode()
  37. {
  38. return HashCode.Combine(JobID, RequiID);
  39. }
  40. }
  41. public class StockForecastOrderData(ProductLink product, ProductStyleLink style, StockDimensions dimensions)
  42. {
  43. public ProductLink Product { get; set; } = product;
  44. public ProductStyleLink Style { get; set; } = style;
  45. public StockDimensions Dimensions { get; set; } = dimensions;
  46. public double RequiredQuantity { get; set; }
  47. public class QuantityBreakup(Guid jobID, Guid requiID, string description, double qty)
  48. {
  49. // This may be blank, in which case this refers to General Stock.
  50. public Guid JobID { get; set; } = jobID;
  51. public Guid JobRequiItemID { get; set; } = requiID;
  52. public string Description { get; set; } = description;
  53. public double Quantity { get; set; } = qty;
  54. }
  55. private List<QuantityBreakup> RequiredQuantities { get; set; } = [];
  56. public List<QuantityBreakup> GetRequiredQuantities() => RequiredQuantities;
  57. public void SetRequiredQuantity(Guid jobID, Guid requiID, string description, double qty)
  58. {
  59. RequiredQuantities.Add(new(jobID, requiID, description,qty));
  60. }
  61. }
  62. public enum StockForecastOrderingType
  63. {
  64. StockOrder,
  65. Breakup
  66. }
  67. public class StockForecastOrderingItemQuantity
  68. {
  69. public event Action? Changed;
  70. private double orderTotal;
  71. public double OrderTotal
  72. {
  73. get => orderTotal;
  74. set
  75. {
  76. orderTotal = value;
  77. Changed?.Invoke();
  78. }
  79. }
  80. public Dictionary<StockForecastBreakupKey, double> Breakups { get; init; } = [];
  81. private SupplierProduct? _supplierProduct;
  82. /// <summary>
  83. /// Indicates the Supplier Product that has been selected for this cell. This comes from the combobox column.
  84. /// </summary>
  85. public SupplierProduct? SupplierProduct
  86. {
  87. get => _supplierProduct;
  88. set
  89. {
  90. _supplierProduct = value;
  91. Changed?.Invoke();
  92. }
  93. }
  94. public void DoChanged()
  95. {
  96. Changed?.Invoke();
  97. }
  98. public double JobTotal => Breakups.Values.Sum();
  99. public double GetTotal(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  100. ? OrderTotal
  101. : JobTotal;
  102. }
  103. public class StockOrderingItem : BaseObject
  104. {
  105. [EditorSequence(1)]
  106. public ProductLink Product { get; set; }
  107. [EditorSequence(2)]
  108. public ProductStyleLink Style { get; set; }
  109. [EditorSequence(3)]
  110. public StockDimensions Dimensions { get; set; }
  111. [EditorSequence(5)]
  112. [DoubleEditor]
  113. public double RequiredQuantity { get; set; }
  114. [EditorSequence(6)]
  115. [EnumLookupEditor(typeof(SupplierProductOrderStrategy))]
  116. public SupplierProductOrderStrategy OrderStrategy { get; set; }
  117. private Dictionary<StockForecastBreakupKey, double> JobRequiredQuantities { get; set; } = new()
  118. {
  119. { new(Guid.Empty, Guid.Empty), 0.0 }
  120. };
  121. public Dictionary<StockForecastBreakupKey, double> GetJobRequiredQuantities()
  122. {
  123. return JobRequiredQuantities;
  124. }
  125. public void SetJobRequiredQuantity(Guid jobID, Guid requiID, double requiredQuantity)
  126. {
  127. JobRequiredQuantities[new(jobID, requiID)] = requiredQuantity;
  128. }
  129. public bool CustomStrategy { get; set; } = false;
  130. public StockForecastOrderingItemQuantity[] Quantities = [];
  131. public StockForecastOrderingItemQuantity GetQuantity(int i) => Quantities[i];
  132. public double GetTotalQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  133. ? Quantities.Sum(x => x.OrderTotal)
  134. : Quantities.Sum(x => x.JobTotal);
  135. public double GetRequiredQuantity(StockForecastOrderingType type) => type == StockForecastOrderingType.StockOrder
  136. ? RequiredQuantity
  137. : JobRequiredQuantities.Values.Sum();
  138. public void SetQuantities(StockForecastOrderingItemQuantity[] quantities)
  139. {
  140. Quantities = quantities;
  141. }
  142. }
  143. public class StockForecastOrderingResult(
  144. SupplierLink supplier,
  145. List<StockForecastOrderData.QuantityBreakup> breakups,
  146. StockOrderingItem item,
  147. double quantity,
  148. SupplierProduct supplierProduct)
  149. {
  150. public SupplierLink Supplier { get; set; } = supplier;
  151. public StockOrderingItem Item { get; set; } = item;
  152. public SupplierProduct SupplierProduct { get; set; } = supplierProduct;
  153. public double Quantity { get; set; } = quantity;
  154. public List<StockForecastOrderData.QuantityBreakup> Breakups { get; set; } = breakups;
  155. }
  156. public enum StockForecastOrderingStrategy
  157. {
  158. PerProduct,
  159. Exact,
  160. RoundUp,
  161. LowestUnitPrice,
  162. LowestOverallPrice,
  163. LowestOverstock
  164. }
  165. public class StockForecastOrderingGrid : DynamicItemsListGrid<StockOrderingItem>, ISpecificGrid
  166. {
  167. #region Internal Data + Caches
  168. private List<SupplierProduct> SupplierProducts = [];
  169. private SupplierLink[] Suppliers = [];
  170. private DynamicActionColumn[] SupplierProductColumns = [];
  171. private DynamicActionColumn[] QuantityColumns = [];
  172. private DynamicActionColumn[] CostColumns = [];
  173. private readonly Dictionary<Guid, Job> JobDetails = [];
  174. private readonly Dictionary<Guid, JobRequisitionItem> JobRequiDetails = [];
  175. #endregion
  176. private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
  177. #region Public Properties
  178. public IList<StockForecastOrderData> OrderData { get; set; }
  179. private StockForecastOrderingType _orderType = StockForecastOrderingType.Breakup;
  180. public StockForecastOrderingType OrderType
  181. {
  182. get => _orderType;
  183. set
  184. {
  185. if(_orderType != value)
  186. {
  187. _orderType = value;
  188. if (OrderData != null)
  189. {
  190. CalculateQuantities(true);
  191. UIComponent.UpdateOrderType(OrderType);
  192. foreach(var control in QuantityControls)
  193. {
  194. control.UpdateControl(OrderType);
  195. }
  196. Refresh(true, true);
  197. }
  198. }
  199. }
  200. }
  201. private StockForecastOrderingStrategy orderStrategy;
  202. public StockForecastOrderingStrategy OrderStrategy
  203. {
  204. get => orderStrategy;
  205. set
  206. {
  207. orderStrategy = value;
  208. if (OrderData != null)
  209. {
  210. foreach (var item in Items)
  211. {
  212. item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(value, item.Product.OrderStrategy);
  213. item.CustomStrategy = false;
  214. }
  215. CalculateQuantities(false);
  216. Refresh(false, true);
  217. }
  218. }
  219. }
  220. public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity(OrderType));
  221. public IEnumerable<StockForecastOrderingResult> Results
  222. {
  223. get
  224. {
  225. for(int i = 0; i < Suppliers.Length; ++i)
  226. {
  227. var supplier = Suppliers[i];
  228. foreach(var item in Items)
  229. {
  230. var qty = item.GetQuantity(i);
  231. if (qty.SupplierProduct is null)
  232. {
  233. continue;
  234. }
  235. if(OrderType == StockForecastOrderingType.StockOrder && qty.OrderTotal > 0)
  236. {
  237. yield return new(supplier, new(), item, qty.OrderTotal, qty.SupplierProduct);
  238. }
  239. else if(qty.OrderTotal > 0)
  240. {
  241. var breakups = new List<StockForecastOrderData.QuantityBreakup>();
  242. foreach(var (key, q) in qty.Breakups)
  243. {
  244. // Check JobID because we are to skip the empty job (this is just the difference between all the allocations and the quantity on the PO).
  245. if(q > 0 && key.JobID != Guid.Empty)
  246. {
  247. breakups.Add(new(key.JobID, key.RequiID, "", q));
  248. }
  249. }
  250. yield return new(supplier, breakups, item, qty.OrderTotal, qty.SupplierProduct);
  251. }
  252. }
  253. }
  254. }
  255. }
  256. #endregion
  257. public StockForecastOrderingGrid()
  258. {
  259. HiddenColumns.Add(x => x.Product.Image.ID);
  260. }
  261. private static SupplierProductOrderStrategy CastOrderStrategyToProductOrderStrategy(StockForecastOrderingStrategy strategy, SupplierProductOrderStrategy defaultValue)
  262. {
  263. return strategy switch
  264. {
  265. StockForecastOrderingStrategy.Exact => SupplierProductOrderStrategy.Exact,
  266. StockForecastOrderingStrategy.LowestOverallPrice => SupplierProductOrderStrategy
  267. .LowestOverallPrice,
  268. StockForecastOrderingStrategy.LowestUnitPrice => SupplierProductOrderStrategy.LowestUnitPrice,
  269. StockForecastOrderingStrategy.LowestOverstock => SupplierProductOrderStrategy.LowestOverstock,
  270. StockForecastOrderingStrategy.RoundUp => SupplierProductOrderStrategy.RoundUp,
  271. StockForecastOrderingStrategy.PerProduct or _ => defaultValue
  272. };
  273. }
  274. #region UI Component
  275. private Component? _uiComponent;
  276. private Component UIComponent
  277. {
  278. get
  279. {
  280. _uiComponent ??= new Component(this);
  281. return _uiComponent;
  282. }
  283. }
  284. protected override IDynamicGridUIComponent<StockOrderingItem> CreateUIComponent()
  285. {
  286. return UIComponent;
  287. }
  288. private class Component : DynamicGridGridUIComponent<StockOrderingItem>
  289. {
  290. private StockForecastOrderingGrid Grid;
  291. public Component(StockForecastOrderingGrid grid)
  292. {
  293. Parent = grid;
  294. Grid = grid;
  295. UpdateOrderType(grid.OrderType);
  296. }
  297. public void UpdateOrderType(StockForecastOrderingType type)
  298. {
  299. DataGrid.FrozenColumnCount = 8;
  300. }
  301. protected override Brush? GetCellSelectionBackgroundBrush()
  302. {
  303. return null;
  304. }
  305. protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
  306. {
  307. var item = Grid.LoadItem(row);
  308. if(column is DynamicActionColumn ac)
  309. {
  310. var qIdx = Grid.QuantityColumns.IndexOf(ac);
  311. var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
  312. if(idx != -1)
  313. {
  314. var supplierProduct = item.GetQuantity(idx).SupplierProduct;
  315. return supplierProduct is null
  316. ? Brushes.Gainsboro
  317. : Brushes.WhiteSmoke;
  318. }
  319. }
  320. return base.GetCellBackground(row, column);
  321. }
  322. }
  323. #endregion
  324. private bool _observing = true;
  325. private void SetObserving(bool observing)
  326. {
  327. _observing = observing;
  328. }
  329. protected override void Changed()
  330. {
  331. if (_observing)
  332. {
  333. base.Changed();
  334. }
  335. }
  336. protected override void DoReconfigure(DynamicGridOptions options)
  337. {
  338. options.Clear();
  339. options.FilterRows = true;
  340. }
  341. private bool _loadedData = false;
  342. private void LoadData()
  343. {
  344. var supplierProductColumns = Columns.None<SupplierProduct>().Add(x => x.ID)
  345. .Add(x => x.SupplierLink.ID)
  346. .Add(x => x.Product.ID)
  347. .Add(x => x.Style.ID)
  348. .Add(x => x.Style.Code)
  349. .Add(x => x.Job.ID)
  350. .Add(x => x.Job.JobNumber)
  351. .Add(x => x.ForeignCurrencyPrice)
  352. .Add(x => x.CostPrice)
  353. .AddDimensionsColumns(x => x.Dimensions)
  354. .Add(x => x.SupplierLink.Code);
  355. SupplierProducts = Client.Query(
  356. new Filter<SupplierProduct>(x => x.Product.ID).InList(OrderData.Select(x => x.Product.ID).ToArray())
  357. .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
  358. supplierProductColumns,
  359. new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
  360. .ToList<SupplierProduct>();
  361. Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
  362. CalculateQuantities(true);
  363. _loadedData = true;
  364. }
  365. private StockForecastOrderingItemQuantity CreateQuantity(int itemIdx)
  366. {
  367. var qty = new StockForecastOrderingItemQuantity();
  368. qty.Changed += () =>
  369. {
  370. if (!_observing) return;
  371. var row = Data.Rows[itemIdx];
  372. InvalidateRow(row);
  373. DoChanged();
  374. };
  375. return qty;
  376. }
  377. private SupplierProduct? CalculateSupplierProduct(StockOrderingItem item, int supplierIdx)
  378. {
  379. var supplierProducts = string.IsNullOrWhiteSpace(item.Dimensions.Unit.Conversion)
  380. ? SupplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions))
  381. : SupplierProducts;
  382. 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);
  383. var qty = item.GetQuantity(supplierIdx);
  384. qty.SupplierProduct = supplierProduct;
  385. qty.OrderTotal = 0;
  386. qty.Breakups.Clear();
  387. foreach(var id in item.GetJobRequiredQuantities().Keys)
  388. {
  389. qty.Breakups[id] = 0;
  390. }
  391. return supplierProduct;
  392. }
  393. private void CalculateSupplierProduct(StockOrderingItem item)
  394. {
  395. var selectedSupplierProducts = new List<SupplierProduct>();
  396. for(int i = 0; i < Suppliers.Length; ++i)
  397. {
  398. var supplierProduct = CalculateSupplierProduct(item, i);
  399. if(supplierProduct is not null)
  400. {
  401. selectedSupplierProducts.Add(supplierProduct);
  402. }
  403. }
  404. var selectedSupplierProduct = SelectSupplierProduct(selectedSupplierProducts, item);
  405. if(selectedSupplierProduct is not null)
  406. {
  407. var supplierIdx = Suppliers.WithIndex()
  408. .FirstOrDefault(x => x.Value.ID == selectedSupplierProduct.SupplierLink.ID, new KeyValuePair<int, SupplierLink>(-1, null)).Key;
  409. if(supplierIdx != -1)
  410. {
  411. var qty = item.GetQuantity(supplierIdx);
  412. if(OrderType == StockForecastOrderingType.Breakup)
  413. {
  414. qty.Breakups.Clear();
  415. foreach(var (id, q) in item.GetJobRequiredQuantities())
  416. {
  417. qty.Breakups[id] = q;
  418. }
  419. }
  420. qty.OrderTotal = GetRequiredQuantity(item, selectedSupplierProduct);
  421. }
  422. }
  423. }
  424. private void CalculateQuantities(bool recreateItems)
  425. {
  426. SetObserving(false);
  427. if (recreateItems)
  428. {
  429. Items.Clear();
  430. foreach(var dataItem in OrderData)
  431. {
  432. var item = new StockOrderingItem();
  433. item.Product.CopyFrom(dataItem.Product);
  434. item.Style.CopyFrom(dataItem.Style);
  435. item.Dimensions.CopyFrom(dataItem.Dimensions);
  436. item.OrderStrategy = CastOrderStrategyToProductOrderStrategy(OrderStrategy, item.Product.OrderStrategy);
  437. item.RequiredQuantity = dataItem.RequiredQuantity;
  438. foreach(var breakup in dataItem.GetRequiredQuantities())
  439. {
  440. item.SetJobRequiredQuantity(breakup.JobID, breakup.JobRequiItemID, breakup.Quantity);
  441. }
  442. Items.Add(item);
  443. }
  444. }
  445. foreach(var (itemIdx, item) in Items.WithIndex())
  446. {
  447. var quantities = new StockForecastOrderingItemQuantity[Suppliers.Length];
  448. for(int i = 0; i < Suppliers.Length; ++i)
  449. {
  450. quantities[i] = CreateQuantity(itemIdx);
  451. }
  452. item.SetQuantities(quantities);
  453. }
  454. foreach(var item in Items)
  455. {
  456. CalculateSupplierProduct(item);
  457. }
  458. SetObserving(true);
  459. DoChanged();
  460. }
  461. #region Order Strategy
  462. private double CalculateSupplierProductRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
  463. {
  464. var supplierIdx = Suppliers.WithIndex().FirstOrDefault(x => x.Value.ID == supplierProduct.ID).Key;
  465. var qty = item.GetQuantity(supplierIdx);
  466. var req = OrderType == StockForecastOrderingType.StockOrder ? item.RequiredQuantity : qty.Breakups.Sum(x => x.Value);
  467. var d = new StockDimensions();
  468. d.CopyFrom(supplierProduct.Dimensions);
  469. var result = DimensionUtils.ConvertDimensions(d, 1.0, (f,c) => Client.Query(f,c));
  470. req = Math.Ceiling(req / result);
  471. return req;
  472. }
  473. private SupplierProduct? SelectSupplierProduct(IEnumerable<SupplierProduct> supplierProducts, StockOrderingItem item)
  474. {
  475. double DimensionsRatio(SupplierProduct x)
  476. {
  477. return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value);
  478. }
  479. switch (item.OrderStrategy)
  480. {
  481. case SupplierProductOrderStrategy.Exact:
  482. case SupplierProductOrderStrategy.RoundUp:
  483. return supplierProducts.Where(x => x.Dimensions.Equals(item.Dimensions))
  484. .MinBy(x => x.CostPrice);
  485. case SupplierProductOrderStrategy.LowestOverallPrice:
  486. return supplierProducts.MinBy(x => x.CostPrice * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x)));
  487. case SupplierProductOrderStrategy.LowestUnitPrice:
  488. return supplierProducts.MinBy(x => x.CostPrice * DimensionsRatio(x));
  489. case SupplierProductOrderStrategy.LowestOverstock:
  490. return supplierProducts.MinBy(x => x.Dimensions.Value * Math.Ceiling(CalculateSupplierProductRequiredQuantity(item, x) * DimensionsRatio(x)));
  491. default:
  492. return null;
  493. }
  494. }
  495. private double GetRequiredQuantity(StockOrderingItem item, SupplierProduct supplierProduct)
  496. {
  497. var requiredQuantity = CalculateSupplierProductRequiredQuantity(item, supplierProduct);
  498. double DimensionsRatio(SupplierProduct x)
  499. {
  500. return item.Dimensions.Value / (x.Dimensions.Value.IsEffectivelyEqual(0.0) ? (item.Dimensions.Value.IsEffectivelyEqual(0.0) ? 1.0 : item.Dimensions.Value) : x.Dimensions.Value);
  501. }
  502. var _strategy = item.CustomStrategy
  503. ? SupplierProductOrderStrategy.LowestOverstock
  504. : item.OrderStrategy;
  505. switch (_strategy)
  506. {
  507. case SupplierProductOrderStrategy.Exact:
  508. return requiredQuantity;
  509. case SupplierProductOrderStrategy.RoundUp:
  510. return Math.Ceiling(requiredQuantity);
  511. case SupplierProductOrderStrategy.LowestOverallPrice:
  512. case SupplierProductOrderStrategy.LowestUnitPrice:
  513. case SupplierProductOrderStrategy.LowestOverstock:
  514. return Math.Ceiling(requiredQuantity * DimensionsRatio(supplierProduct));
  515. default:
  516. return 0.0;
  517. }
  518. }
  519. #endregion
  520. private bool _loadedColumns = false;
  521. protected override DynamicGridColumns LoadColumns()
  522. {
  523. if (!_loadedData)
  524. {
  525. LoadData();
  526. }
  527. var columns = new DynamicGridColumns();
  528. columns.Add<StockOrderingItem, string>(x => x.Product.Code, 120, "Product Code", "", Alignment.MiddleCenter);
  529. columns.Add<StockOrderingItem, string>(x => x.Product.Name, 0, "Product Name", "", Alignment.MiddleLeft);
  530. columns.Add<StockOrderingItem, string>(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter);
  531. columns.Add<StockOrderingItem, string>(x => x.Style.Code, 80, "Style", "", Alignment.MiddleCenter);
  532. columns.Add<StockOrderingItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
  533. if (!_loadedColumns)
  534. {
  535. ActionColumns.Clear();
  536. ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
  537. ActionColumns.Add(new DynamicImagePreviewColumn<StockOrderingItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
  538. ActionColumns.Add(new DynamicTemplateColumn(row =>
  539. {
  540. var item = LoadItem(row);
  541. var box = new ComboBox();
  542. box.ItemsSource = Enum.GetValues<SupplierProductOrderStrategy>()
  543. .Select(x => new KeyValuePair<SupplierProductOrderStrategy, string>(x, CoreUtils.Neatify(x.ToString())));
  544. box.DisplayMemberPath = "Value";
  545. box.SelectedValuePath = "Key";
  546. box.SelectedValue = item.CustomStrategy ? null : item.OrderStrategy;
  547. box.SelectionChanged += (o, e) =>
  548. {
  549. if (box.SelectedValue is not SupplierProductOrderStrategy strategy) return;
  550. item.OrderStrategy = strategy;
  551. item.CustomStrategy = false;
  552. CalculateSupplierProduct(item);
  553. InvalidateRow(row);
  554. };
  555. box.Margin = new Thickness(2);
  556. box.VerticalContentAlignment = VerticalAlignment.Center;
  557. return box;
  558. })
  559. {
  560. HeaderText = "Order Strategy.",
  561. Width = 140
  562. });
  563. SupplierProductColumns = new DynamicActionColumn[Suppliers.Length];
  564. QuantityColumns = new DynamicActionColumn[Suppliers.Length];
  565. CostColumns = new DynamicActionColumn[Suppliers.Length];
  566. QuantityControls.Clear();
  567. for(int i = 0; i < Suppliers.Length; ++i)
  568. {
  569. InitialiseSupplierColumn(i);
  570. }
  571. ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
  572. _loadedColumns = true;
  573. }
  574. return columns;
  575. }
  576. private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
  577. {
  578. grid.OnCustomiseEditor += (sender, items, column, editor) =>
  579. {
  580. if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
  581. || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
  582. || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName))
  583. {
  584. editor.Editable = editor.Editable.Combine(Editable.Disabled);
  585. }
  586. };
  587. }
  588. private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
  589. {
  590. if (row is null) return;
  591. column.AddItem("New Supplier", null, row =>
  592. {
  593. if (row is null) return;
  594. var selection = new MultiSelectDialog<Supplier>(
  595. new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
  596. Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
  597. if (selection.ShowDialog() != true)
  598. {
  599. return;
  600. }
  601. var supplier = selection.Data().Rows.First().ToObject<Supplier>();
  602. var orderingItem = LoadItem(row);
  603. var supplierProduct = new SupplierProduct();
  604. supplierProduct.Product.CopyFrom(orderingItem.Product);
  605. supplierProduct.Style.CopyFrom(orderingItem.Style);
  606. supplierProduct.Dimensions.CopyFrom(orderingItem.Dimensions);
  607. supplierProduct.SupplierLink.CopyFrom(supplier);
  608. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  609. {
  610. SupplierProducts.Add(supplierProduct);
  611. var newSuppliers = new SupplierLink[Suppliers.Length + 1];
  612. var newIdx = Suppliers.Length;
  613. for (int i = 0; i < Suppliers.Length; i++)
  614. {
  615. newSuppliers[i] = Suppliers[i];
  616. }
  617. newSuppliers[newIdx] = supplierProduct.SupplierLink;
  618. foreach (var (itemIdx, item) in Items.WithIndex())
  619. {
  620. var quantities = new StockForecastOrderingItemQuantity[newSuppliers.Length];
  621. for (int i = 0; i < Suppliers.Length; ++i)
  622. {
  623. quantities[i] = item.GetQuantity(i);
  624. }
  625. var newQty = CreateQuantity(itemIdx);
  626. quantities[newIdx] = newQty;
  627. if(OrderType == StockForecastOrderingType.StockOrder)
  628. {
  629. newQty.OrderTotal = 0;
  630. }
  631. else
  632. {
  633. newQty.OrderTotal = 0;
  634. foreach(var id in item.GetJobRequiredQuantities().Keys)
  635. {
  636. newQty.Breakups[id] = 0;
  637. }
  638. }
  639. item.SetQuantities(quantities);
  640. }
  641. Suppliers = newSuppliers;
  642. foreach (var item in Items)
  643. {
  644. CalculateSupplierProduct(item, newIdx);
  645. }
  646. _loadedColumns = false;
  647. Refresh(true, true);
  648. }
  649. });
  650. }
  651. private BitmapImage? Warning_Image(CoreRow? row)
  652. {
  653. if (row is null) return _warning;
  654. var item = LoadItem(row);
  655. if(item.GetTotalQuantity(OrderType) < item.RequiredQuantity)
  656. {
  657. return _warning;
  658. }
  659. else
  660. {
  661. return null;
  662. }
  663. }
  664. protected override void ConfigureColumnGroups()
  665. {
  666. for(int idx = 0; idx < Suppliers.Length; ++idx)
  667. {
  668. GetColumnGrouping().AddGroup(Suppliers[idx].Code, SupplierProductColumns[idx], CostColumns[idx]);
  669. }
  670. }
  671. #region Job Data Cache
  672. private void LoadJobData(IEnumerable<Guid> ids)
  673. {
  674. var neededIDs = ids.Where(x => x != Guid.Empty && !JobDetails.ContainsKey(x)).ToArray();
  675. if(neededIDs.Length > 0)
  676. {
  677. var details = Client.Query(
  678. new Filter<Job>(x => x.ID).InList(neededIDs),
  679. Columns.None<Job>().Add(x => x.ID)
  680. .Add(x => x.JobNumber)
  681. .Add(x => x.Name));
  682. foreach(var job in details.ToObjects<Job>())
  683. {
  684. JobDetails[job.ID] = job;
  685. }
  686. }
  687. }
  688. private void LoadJobRequiData(IEnumerable<Guid> ids)
  689. {
  690. var neededIDs = ids.Where(x => x != Guid.Empty && !JobRequiDetails.ContainsKey(x)).ToArray();
  691. if(neededIDs.Length > 0)
  692. {
  693. var details = Client.Query(
  694. new Filter<JobRequisitionItem>(x => x.ID).InList(neededIDs),
  695. Columns.None<JobRequisitionItem>().Add(x => x.ID)
  696. .Add(x => x.Requisition.Number)
  697. .Add(x => x.Requisition.Description));
  698. foreach(var requi in details.ToObjects<JobRequisitionItem>())
  699. {
  700. JobRequiDetails[requi.ID] = requi;
  701. }
  702. }
  703. }
  704. #endregion
  705. private class QuantityControl : ContentControl
  706. {
  707. private readonly StockOrderingItem Item;
  708. private readonly int SupplierIndex;
  709. private readonly StockForecastOrderingGrid Parent;
  710. public QuantityControl(StockForecastOrderingGrid parent, StockOrderingItem item, int supplierIndex, StockForecastOrderingType mode)
  711. {
  712. Parent = parent;
  713. Item = item;
  714. SupplierIndex = supplierIndex;
  715. UpdateControl(mode);
  716. }
  717. public void UpdateControl(StockForecastOrderingType mode)
  718. {
  719. // If no supplier product has been selected for this cell, we can't allow the user to select a quantity.
  720. var supplierProduct = Item.GetQuantity(SupplierIndex).SupplierProduct;
  721. if(supplierProduct is null)
  722. {
  723. Content = null;
  724. return;
  725. }
  726. if(mode == StockForecastOrderingType.StockOrder)
  727. {
  728. var editor = new DoubleTextBox
  729. {
  730. VerticalAlignment = VerticalAlignment.Stretch,
  731. HorizontalAlignment = HorizontalAlignment.Stretch,
  732. Background = new SolidColorBrush(Colors.LightYellow),
  733. BorderThickness = new Thickness(0.0),
  734. MinValue = 0.0,
  735. Value = Item.GetQuantity(SupplierIndex).OrderTotal
  736. };
  737. editor.ValueChanged += (o, e) =>
  738. {
  739. Item.GetQuantity(SupplierIndex).OrderTotal = editor.Value ?? default;
  740. };
  741. Content = editor;
  742. }
  743. else if(mode == StockForecastOrderingType.Breakup)
  744. {
  745. var grid = new Grid();
  746. grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
  747. grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
  748. var _breakups = Item.GetQuantity(SupplierIndex).Breakups.Sum(x => x.Value);
  749. var editor = new DoubleTextBox
  750. {
  751. VerticalAlignment = VerticalAlignment.Stretch,
  752. HorizontalAlignment = HorizontalAlignment.Stretch,
  753. VerticalContentAlignment = VerticalAlignment.Center,
  754. HorizontalContentAlignment = HorizontalAlignment.Center,
  755. Background = new SolidColorBrush(Colors.WhiteSmoke),
  756. BorderThickness = new Thickness(0.0),
  757. Value = Item.GetQuantity(SupplierIndex).OrderTotal,
  758. IsReadOnly = true,
  759. Focusable = false,
  760. };
  761. Grid.SetColumn(editor, 0);
  762. grid.Children.Add(editor);
  763. var btn = new Button
  764. {
  765. VerticalAlignment = VerticalAlignment.Stretch,
  766. VerticalContentAlignment = VerticalAlignment.Center,
  767. HorizontalAlignment = HorizontalAlignment.Stretch,
  768. Content = "..",
  769. Margin = new Thickness(1),
  770. Focusable = false
  771. };
  772. btn.SetValue(Grid.ColumnProperty, 1);
  773. btn.SetValue(Grid.RowProperty, 0);
  774. btn.Click += (o, e) =>
  775. {
  776. var qty = Item.GetQuantity(SupplierIndex);
  777. Parent.LoadJobData(qty.Breakups.Keys.Select(x => x.JobID));
  778. Parent.LoadJobRequiData(qty.Breakups.Keys.Select(x => x.RequiID));
  779. var items = qty.Breakups.Select(x =>
  780. {
  781. var item = new StockForecastOrderingJobItem
  782. {
  783. JobID = x.Key.JobID,
  784. JobRequiID = x.Key.RequiID,
  785. RequiredQuantity = Item.GetJobRequiredQuantities().GetValueOrDefault(x.Key),
  786. Quantity = x.Value
  787. };
  788. if(item.JobID == Guid.Empty)
  789. {
  790. item.Description = "General Stock";
  791. }
  792. else if(Parent.JobDetails.TryGetValue(item.JobID, out var job))
  793. {
  794. if(Parent.JobRequiDetails.TryGetValue(item.JobRequiID, out var requi))
  795. {
  796. item.Description = $"{job.JobNumber}: Requi #{requi.Requisition.Number} ({requi.Requisition.Description})";
  797. }
  798. else
  799. {
  800. item.Description = $"{job.JobNumber}: {job.Name}";
  801. }
  802. }
  803. return item;
  804. }).ToList();
  805. var genitem = items.FirstOrDefault(x =>
  806. x.JobID == Guid.Empty && x.JobRequiID == Guid.Empty);
  807. if (genitem == null)
  808. {
  809. genitem = new StockForecastOrderingJobItem
  810. {
  811. Description = "General Stock",
  812. JobID = Guid.Empty,
  813. JobRequiID = Guid.Empty
  814. };
  815. items.Insert(0, genitem);
  816. }
  817. else
  818. {
  819. items.Remove(genitem);
  820. items.Insert(0, genitem);
  821. }
  822. var window = new StockForecastOrderJobScreen();
  823. window.Items = items;
  824. if(window.ShowDialog() == true)
  825. {
  826. foreach(var item in items)
  827. {
  828. qty.Breakups[new(item.JobID, item.JobRequiID)] = item.Quantity;
  829. }
  830. qty.DoChanged();
  831. var req = Parent.GetRequiredQuantity(Item, supplierProduct);
  832. editor.Value = req;
  833. qty.OrderTotal = req;
  834. }
  835. };
  836. grid.Children.Add(btn);
  837. Content = grid;
  838. }
  839. }
  840. }
  841. private List<QuantityControl> QuantityControls = [];
  842. private void InitialiseSupplierColumn(int idx)
  843. {
  844. var contextMenuFunc = (CoreRow[]? rows) =>
  845. {
  846. var row = rows?.FirstOrDefault();
  847. if (row is null) return null;
  848. var item = LoadItem(row);
  849. var menu = new ContextMenu();
  850. menu.AddItem("Create Supplier Product", null, new Tuple<StockOrderingItem, int>(item, idx), CreateSupplierProduct_Click);
  851. return menu;
  852. };
  853. var qtyColumn = new Tuple<DynamicActionColumn, QuantityControl?>(null!, null);
  854. SupplierProductColumns[idx] = new DynamicTemplateColumn(row =>
  855. {
  856. var instance = LoadItem(row);
  857. var comboBox = new ComboBox();
  858. comboBox.Tag = idx;
  859. var supplierProducts = string.IsNullOrWhiteSpace(instance.Dimensions.Unit.Conversion)
  860. ? SupplierProducts.Where(x => x.Dimensions.Equals(instance.Dimensions))
  861. : SupplierProducts;
  862. var items = supplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID && x.Product.ID == instance.Product.ID)
  863. .Select(x => new KeyValuePair<SupplierProduct?, string>(x, x.Job.ID == Guid.Empty ? x.Dimensions.UnitSize : $"Job {x.Job.JobNumber}: {x.Dimensions.UnitSize}"));
  864. if (items.Any())
  865. items = items.Prepend(new KeyValuePair<SupplierProduct?, string>(null, ""));
  866. comboBox.SelectedValuePath = "Key";
  867. comboBox.ItemsSource = items.ToArray();
  868. comboBox.DisplayMemberPath = "Value";
  869. var qty = instance.GetQuantity(idx);
  870. comboBox.Bind(ComboBox.SelectedValueProperty, qty, x => x.SupplierProduct);
  871. comboBox.SelectionChanged += (o, e) =>
  872. {
  873. var box = o as ComboBox;
  874. instance.CustomStrategy = true;
  875. var _item = LoadItem(row);
  876. var _product = ((o as ComboBox)?.SelectedValue as SupplierProduct ?? new SupplierProduct());
  877. qty.OrderTotal = GetRequiredQuantity(_item, _product);
  878. InvalidateRow(row);
  879. };
  880. comboBox.VerticalContentAlignment = VerticalAlignment.Center;
  881. comboBox.Margin = new Thickness(2);
  882. if(!items.Any())
  883. comboBox.IsEnabled = false;
  884. return comboBox;
  885. })
  886. {
  887. HeaderText = "U.O.M.",
  888. Width = 80
  889. };
  890. QuantityColumns[idx] = new DynamicTemplateColumn(row =>
  891. {
  892. var instance = LoadItem(row);
  893. var control = new QuantityControl(this, instance, idx, OrderType);
  894. QuantityControls.Add(control);
  895. return control;
  896. })
  897. {
  898. HeaderText = "Qty.",
  899. Width = 80,
  900. ContextMenu = contextMenuFunc
  901. };
  902. CostColumns[idx] = new DynamicTextColumn(row =>
  903. {
  904. if(row is null)
  905. {
  906. return "Cost";
  907. }
  908. var instance = LoadItem(row);
  909. var qty = instance.GetQuantity(idx);
  910. if(qty.SupplierProduct is not null)
  911. {
  912. return $"{qty.OrderTotal * qty.SupplierProduct.CostPrice:C2}";
  913. }
  914. else
  915. {
  916. return "";
  917. }
  918. })
  919. {
  920. HeaderText = "Cost",
  921. Width = 80,
  922. ContextMenu = contextMenuFunc,
  923. GetSummary = () =>
  924. {
  925. var summary = new GridSummaryColumn
  926. {
  927. Format = "{Sum:C2}",
  928. SummaryType = Syncfusion.Data.SummaryType.Custom,
  929. CustomAggregate = new CostAggregate(idx, this)
  930. };
  931. return summary;
  932. }
  933. };
  934. ActionColumns.Add(SupplierProductColumns[idx]);
  935. ActionColumns.Add(QuantityColumns[idx]);
  936. ActionColumns.Add(CostColumns[idx]);
  937. }
  938. private void CreateSupplierProduct_Click(Tuple<StockOrderingItem, int> tuple)
  939. {
  940. var (item, supplierIdx) = tuple;
  941. var supplierProduct = new SupplierProduct();
  942. supplierProduct.Product.CopyFrom(item.Product);
  943. supplierProduct.Style.CopyFrom(item.Style);
  944. supplierProduct.Dimensions.CopyFrom(item.Dimensions);
  945. supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]);
  946. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  947. {
  948. SupplierProducts.Add(supplierProduct);
  949. var qty = item.GetQuantity(supplierIdx);
  950. if(qty.SupplierProduct is null)
  951. {
  952. CalculateSupplierProduct(item, supplierIdx);
  953. }
  954. InvalidateGrid();
  955. }
  956. }
  957. private class CostAggregate : ISummaryAggregate
  958. {
  959. public double Sum { get; private set; }
  960. private int SupplierIndex;
  961. private StockForecastOrderingGrid Grid;
  962. public CostAggregate(int supplierIndex, StockForecastOrderingGrid grid)
  963. {
  964. SupplierIndex = supplierIndex;
  965. Grid = grid;
  966. }
  967. public Action<IEnumerable, string, PropertyDescriptor> CalculateAggregateFunc()
  968. {
  969. return AggregateFunc;
  970. }
  971. private void AggregateFunc(IEnumerable items, string property, PropertyDescriptor args)
  972. {
  973. if (items is IEnumerable<DataRowView> rows)
  974. {
  975. Sum = 0;
  976. foreach (var dataRow in rows)
  977. {
  978. var rowIdx = dataRow.Row.Table.Rows.IndexOf(dataRow.Row);
  979. var item = Grid.LoadItem(Grid.Data.Rows[rowIdx]);
  980. var qty = item.GetQuantity(SupplierIndex);
  981. if(qty.SupplierProduct is not null)
  982. {
  983. Sum += qty.OrderTotal * qty.SupplierProduct.CostPrice;
  984. }
  985. }
  986. }
  987. else
  988. {
  989. Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'.");
  990. }
  991. }
  992. }
  993. }