StockForecastOrderingGrid.cs 40 KB

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