StockForecastOrderingGrid.cs 41 KB

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