StockForecastOrderingGrid.cs 40 KB

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