ReservationManagementTreatmentOrderGrid.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using InABox.DynamicGrid;
  5. using InABox.Wpf;
  6. using InABox.WPF;
  7. using NPOI.SS.Formula.Functions;
  8. using Syncfusion.Data;
  9. using Syncfusion.Data.Extensions;
  10. using Syncfusion.UI.Xaml.Grid;
  11. using Syncfusion.Windows.Shared;
  12. using System;
  13. using System.Collections;
  14. using System.Collections.Generic;
  15. using System.ComponentModel;
  16. using System.Data;
  17. using System.Linq;
  18. using System.Text;
  19. using System.Threading.Tasks;
  20. using System.Windows;
  21. using System.Windows.Controls;
  22. using System.Windows.Media;
  23. using System.Windows.Media.Imaging;
  24. using Columns = InABox.Core.Columns;
  25. namespace PRSDesktop.Panels.ReservationManagement.TreatmentPO;
  26. public class ReservationManagementTreatmentPOItem : BaseObject
  27. {
  28. public delegate void ChangedHandler(int idx, double quantity);
  29. public event ChangedHandler? Changed;
  30. public ProductLink TreatmentProduct { get; set; }
  31. public ProductStyleLink Finish { get; set; }
  32. public JobLink Job { get; set; }
  33. public ProductLink Product { get; set; }
  34. public ProductStyleLink Style { get; set; }
  35. public StockDimensions Dimensions { get; set; }
  36. public StockLocationLink Location { get; set; }
  37. public JobRequisitionItemLink JRI { get; set; }
  38. public double Multiplier { get; set; }
  39. [EditorSequence(4)]
  40. [DoubleEditor]
  41. public double RequiredQuantity { get; set; }
  42. private double[] Quantities = [];
  43. public double GetQuantity(int i) => Quantities[i];
  44. public void SetQuantity(int i, double quantity)
  45. {
  46. Quantities[i] = quantity;
  47. Changed?.Invoke(i, quantity);
  48. }
  49. public double GetTotalQuantity() => Quantities.Sum();
  50. public void SetQuantities(double[] quantities)
  51. {
  52. Quantities = quantities;
  53. }
  54. }
  55. public class StockForecastOrderingResult
  56. {
  57. public SupplierLink Supplier { get; set; }
  58. public ReservationManagementTreatmentPOItem Item { get; set; }
  59. public SupplierProduct SupplierProduct { get; set; }
  60. public double Quantity { get; set; }
  61. public StockForecastOrderingResult(SupplierLink supplier, ReservationManagementTreatmentPOItem item, double quantity, SupplierProduct supplierProduct)
  62. {
  63. Supplier = supplier;
  64. Item = item;
  65. Quantity = quantity;
  66. SupplierProduct = supplierProduct;
  67. }
  68. }
  69. public class ReservationManagementTreatmentOrderGrid: DynamicItemsListGrid<ReservationManagementTreatmentPOItem>, ISpecificGrid
  70. {
  71. private List<SupplierProduct> SupplierProducts = [];
  72. private SupplierLink[] Suppliers = [];
  73. public double TotalQuantity => Items.Sum(x => x.GetTotalQuantity());
  74. private DynamicActionColumn[] QuantityColumns = [];
  75. private DynamicActionColumn[] CostColumns = [];
  76. private readonly Dictionary<Guid, Job> JobDetails = [];
  77. private static BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
  78. public IEnumerable<StockForecastOrderingResult> Results
  79. {
  80. get
  81. {
  82. for(int i = 0; i < Suppliers.Length; ++i)
  83. {
  84. var supplier = Suppliers[i];
  85. foreach(var item in Items)
  86. {
  87. var qty = item.GetQuantity(i);
  88. if(qty == 0)
  89. {
  90. continue;
  91. }
  92. var supplierProduct = GetSupplierProduct(item, supplier.ID);
  93. if (supplierProduct is null)
  94. {
  95. continue;
  96. }
  97. yield return new(supplier, item, qty, supplierProduct);
  98. }
  99. }
  100. }
  101. }
  102. private DynamicGridCustomColumnsComponent<ReservationManagementTreatmentPOItem> ColumnsComponent;
  103. public ReservationManagementTreatmentOrderGrid()
  104. {
  105. HiddenColumns.Add(x => x.Product.Image.ID);
  106. ColumnsComponent = new(this, nameof(ReservationManagementTreatmentOrderGrid));
  107. }
  108. #region UI Component
  109. private Component? _uiComponent;
  110. private Component UIComponent
  111. {
  112. get
  113. {
  114. _uiComponent ??= new Component(this);
  115. return _uiComponent;
  116. }
  117. }
  118. protected override IDynamicGridUIComponent<ReservationManagementTreatmentPOItem> CreateUIComponent()
  119. {
  120. return UIComponent;
  121. }
  122. private class Component : DynamicGridGridUIComponent<ReservationManagementTreatmentPOItem>
  123. {
  124. private ReservationManagementTreatmentOrderGrid Grid;
  125. public Component(ReservationManagementTreatmentOrderGrid grid)
  126. {
  127. Parent = grid;
  128. Grid = grid;
  129. DataGrid.FrozenColumnCount = 9;
  130. }
  131. protected override Brush? GetCellSelectionBackgroundBrush()
  132. {
  133. return null;
  134. }
  135. protected override Brush? GetCellBackground(CoreRow row, DynamicColumnBase column)
  136. {
  137. var item = Grid.LoadItem(row);
  138. if(column is DynamicActionColumn ac)
  139. {
  140. var qIdx = Grid.QuantityColumns.IndexOf(ac);
  141. var idx = Math.Max(qIdx, Grid.CostColumns.IndexOf(ac));
  142. if(idx != -1)
  143. {
  144. var supplierProduct = Grid.GetSupplierProduct(item, Grid.Suppliers[idx].ID);
  145. if(supplierProduct is null)
  146. {
  147. return new SolidColorBrush(Colors.Gainsboro);
  148. }
  149. //if(item.GetTotalQuantity(Grid.OrderType) < item.RequiredQuantity)
  150. //{
  151. // return new SolidColorBrush(Colors.LightSalmon) { Opacity = 0.5 };
  152. //}
  153. //else
  154. //{
  155. // return new SolidColorBrush(Colors.LightGreen) { Opacity = 0.5 };
  156. //}
  157. }
  158. }
  159. return base.GetCellBackground(row, column);
  160. }
  161. }
  162. #endregion
  163. private bool _observing = true;
  164. private void SetObserving(bool observing)
  165. {
  166. _observing = observing;
  167. }
  168. protected override void Changed()
  169. {
  170. if (_observing)
  171. {
  172. base.Changed();
  173. }
  174. }
  175. protected override void DoReconfigure(DynamicGridOptions options)
  176. {
  177. options.Clear();
  178. options.FilterRows = true;
  179. }
  180. private bool _loadedData = false;
  181. private void LoadData()
  182. {
  183. var supplierColumns = Columns.None<SupplierProduct>().Add(x => x.ID)
  184. .Add(x => x.SupplierLink.ID)
  185. .Add(x => x.Job.ID)
  186. .Add(x => x.Product.ID)
  187. .Add(x => x.CostPrice)
  188. .Add(x => x.SupplierLink.Code)
  189. .Add(x => x.Product.TaxCode.ID)
  190. .Add(x => x.TaxCode.ID);
  191. SupplierProducts = Client.Query(
  192. new Filter<SupplierProduct>(x => x.Product.ID).InList(Items.Select(x => x.TreatmentProduct.ID).ToArray())
  193. .And(x => x.SupplierLink.ID).IsNotEqualTo(Guid.Empty),
  194. supplierColumns,
  195. new SortOrder<SupplierProduct>(x => x.SupplierLink.Code))
  196. .ToList<SupplierProduct>();
  197. Suppliers = SupplierProducts.Select(x => x.SupplierLink).DistinctBy(x => x.ID).ToArray();
  198. foreach(var (itemIdx, item) in Items.WithIndex())
  199. {
  200. var quantities = new double[Suppliers.Length];
  201. for(int i = 0; i < Suppliers.Length; ++i)
  202. {
  203. quantities[i] = 0;
  204. }
  205. item.SetQuantities(quantities);
  206. item.Changed += (i, qty) =>
  207. {
  208. if (!_observing) return;
  209. var row = Data.Rows[itemIdx];
  210. foreach(var ac in ActionColumns)
  211. {
  212. if(ac != QuantityColumns[i])
  213. {
  214. UpdateCell(row, ac);
  215. }
  216. }
  217. DoChanged();
  218. };
  219. }
  220. CalculateQuantities();
  221. _loadedData = true;
  222. }
  223. private void CalculateQuantities()
  224. {
  225. SetObserving(false);
  226. foreach(var item in Items)
  227. {
  228. var supplierProduct = GetSupplierProduct(item);
  229. for(int i = 0; i < Suppliers.Length; ++i)
  230. {
  231. var supplier = Suppliers[i];
  232. if(supplierProduct is not null && supplier.ID == supplierProduct.SupplierLink.ID)
  233. {
  234. item.SetQuantity(i, item.RequiredQuantity);
  235. }
  236. else
  237. {
  238. item.SetQuantity(i, 0);
  239. }
  240. }
  241. }
  242. SetObserving(true);
  243. DoChanged();
  244. InvalidateGrid();
  245. }
  246. protected override void SaveColumns(DynamicGridColumns columns)
  247. {
  248. ColumnsComponent.SaveColumns(columns);
  249. }
  250. protected override void LoadColumnsMenu(ContextMenu menu)
  251. {
  252. ColumnsComponent.LoadColumnsMenu(menu);
  253. }
  254. public override DynamicGridColumns GenerateColumns()
  255. {
  256. var columns = new DynamicGridColumns();
  257. columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Product.Code, 120, "Product", "", Alignment.MiddleCenter);
  258. columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Style.Code, 120, "Style", "", Alignment.MiddleCenter);
  259. //columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Location.Code, 80, "Location", "", Alignment.MiddleLeft);
  260. columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.Dimensions.UnitSize, 80, "Size", "", Alignment.MiddleCenter);
  261. columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.TreatmentProduct.Code, 120, "Finish", "", Alignment.MiddleCenter);
  262. columns.Add<ReservationManagementTreatmentPOItem, string>(x => x.TreatmentProduct.Name, 0, "Treatment Product", "", Alignment.MiddleCenter);
  263. columns.Add<ReservationManagementTreatmentPOItem, double>(x => x.RequiredQuantity, 80, "Required", "", Alignment.MiddleCenter);
  264. columns.Add<ReservationManagementTreatmentPOItem, double>(x => x.Multiplier, 80, "Multiplier", "", Alignment.MiddleCenter);
  265. return columns;
  266. }
  267. protected override DynamicGridColumns LoadColumns()
  268. {
  269. if (!_loadedData)
  270. {
  271. LoadData();
  272. }
  273. ActionColumns.Clear();
  274. ActionColumns.Add(new DynamicImageColumn(Warning_Image) { Position = DynamicActionColumnPosition.Start });
  275. ActionColumns.Add(new DynamicImagePreviewColumn<ReservationManagementTreatmentPOItem>(x => x.Product.Image) { Position = DynamicActionColumnPosition.Start });
  276. var columns = ColumnsComponent.LoadColumns();
  277. QuantityColumns = new DynamicActionColumn[Suppliers.Length];
  278. CostColumns = new DynamicActionColumn[Suppliers.Length];
  279. QuantityControls.Clear();
  280. for(int i = 0; i < Suppliers.Length; ++i)
  281. {
  282. InitialiseSupplierColumn(i);
  283. }
  284. ActionColumns.Add(new DynamicMenuColumn(BuildMenu));
  285. return columns;
  286. }
  287. private void EditSupplierProductGrid(DynamicGrid<SupplierProduct> grid)
  288. {
  289. grid.OnCustomiseEditor += (sender, items, column, editor) =>
  290. {
  291. if(new Column<SupplierProduct>(x => x.SupplierLink.ID).IsEqualTo(column.ColumnName)
  292. || new Column<SupplierProduct>(x => x.Product.ID).IsEqualTo(column.ColumnName)
  293. || new Column<SupplierProduct>(x => x.Style.ID).IsEqualTo(column.ColumnName)
  294. || new Column<SupplierProduct>(x => x.Job.ID).IsEqualTo(column.ColumnName)
  295. //|| new Column<SupplierProduct>(x => x.Dimensions).IsEqualTo(column.ColumnName)
  296. )
  297. {
  298. editor.Editable = editor.Editable.Combine(Editable.Disabled);
  299. }
  300. };
  301. }
  302. private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
  303. {
  304. if (row is null) return;
  305. column.AddItem("New Supplier", null, row =>
  306. {
  307. if (row is null) return;
  308. var selection = new MultiSelectDialog<Supplier>(
  309. new Filter<Supplier>(x => x.ID).NotInList(Suppliers.Select(x => x.ID).ToArray()),
  310. Columns.None<Supplier>().Add(x => x.ID).Add(x => x.Code), multiselect: false);
  311. if (selection.ShowDialog() != true)
  312. {
  313. return;
  314. }
  315. var supplier = selection.Data().Rows.First().ToObject<Supplier>();
  316. var item = LoadItem(row);
  317. var supplierProduct = new SupplierProduct();
  318. LookupFactory.DoLookup<SupplierProduct, Product, ProductLink>(supplierProduct, x => x.Product, item.TreatmentProduct.ID);
  319. supplierProduct.SupplierLink.CopyFrom(supplier);
  320. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  321. {
  322. SupplierProducts.Add(supplierProduct);
  323. var newSuppliers = new SupplierLink[Suppliers.Length + 1];
  324. var newIdx = Suppliers.Length;
  325. for (int i = 0; i < Suppliers.Length; i++)
  326. {
  327. newSuppliers[i] = Suppliers[i];
  328. }
  329. newSuppliers[newIdx] = supplierProduct.SupplierLink;
  330. foreach (var (itemIdx, oItem) in Items.WithIndex())
  331. {
  332. var populateSupplierProduct = GetSupplierProduct(oItem);
  333. var quantities = new double[newSuppliers.Length];
  334. for (int i = 0; i < Suppliers.Length; ++i)
  335. {
  336. quantities[i] = oItem.GetQuantity(i);
  337. }
  338. quantities[newIdx] = 0;
  339. oItem.SetQuantities(quantities);
  340. }
  341. Suppliers = newSuppliers;
  342. Refresh(true, true);
  343. }
  344. });
  345. }
  346. private BitmapImage? Warning_Image(CoreRow? row)
  347. {
  348. if (row is null) return _warning;
  349. var item = LoadItem(row);
  350. if(item.GetTotalQuantity() < item.RequiredQuantity)
  351. {
  352. return _warning;
  353. }
  354. else
  355. {
  356. return null;
  357. }
  358. }
  359. protected override void ConfigureColumnGroups()
  360. {
  361. for(int idx = 0; idx < Suppliers.Length; ++idx)
  362. {
  363. GetColumnGrouping().AddGroup(Suppliers[idx].Code, QuantityColumns[idx], CostColumns[idx]);
  364. }
  365. }
  366. private class QuantityControl : ContentControl
  367. {
  368. private readonly ReservationManagementTreatmentPOItem Item;
  369. private readonly int SupplierIndex;
  370. private readonly ReservationManagementTreatmentOrderGrid Parent;
  371. public QuantityControl(ReservationManagementTreatmentOrderGrid parent, ReservationManagementTreatmentPOItem item, int supplierIndex)
  372. {
  373. Parent = parent;
  374. Item = item;
  375. SupplierIndex = supplierIndex;
  376. UpdateControl();
  377. }
  378. public void UpdateControl()
  379. {
  380. var supplierProduct = Parent.GetSupplierProduct(Item, Parent.Suppliers[SupplierIndex].ID);
  381. if(supplierProduct is null)
  382. {
  383. Content = null;
  384. return;
  385. }
  386. var editor = new DoubleTextBox
  387. {
  388. VerticalAlignment = VerticalAlignment.Stretch,
  389. HorizontalContentAlignment = HorizontalAlignment.Center,
  390. HorizontalAlignment = HorizontalAlignment.Stretch,
  391. Background = new SolidColorBrush(Colors.LightYellow),
  392. BorderThickness = new Thickness(0.0),
  393. MinValue = 0.0,
  394. Value = Item.GetQuantity(SupplierIndex)
  395. };
  396. editor.ValueChanged += (o, e) =>
  397. {
  398. var maxValue = Item.RequiredQuantity - (Item.GetTotalQuantity() - Item.GetQuantity(SupplierIndex));
  399. var value = (double?)e.NewValue ?? default;
  400. if(value > maxValue)
  401. {
  402. Dispatcher.BeginInvoke(() => editor.Value = maxValue);
  403. }
  404. else
  405. {
  406. Item.SetQuantity(SupplierIndex, value);
  407. }
  408. };
  409. Content = editor;
  410. }
  411. }
  412. private List<QuantityControl> QuantityControls = [];
  413. private void InitialiseSupplierColumn(int idx)
  414. {
  415. var supplierProducts = SupplierProducts.Where(x => x.SupplierLink.ID == Suppliers[idx].ID).ToArray();
  416. var contextMenuFunc = (CoreRow[]? rows) =>
  417. {
  418. var row = rows?.FirstOrDefault();
  419. if (row is null) return null;
  420. var item = LoadItem(row);
  421. var supplierProduct = GetSupplierProduct(item, Suppliers[idx].ID);
  422. if (supplierProduct is not null)
  423. {
  424. return null;
  425. }
  426. var menu = new ContextMenu();
  427. menu.AddItem("Create Supplier Product", null, new Tuple<ReservationManagementTreatmentPOItem, int>(item, idx), CreateSupplierProduct_Click);
  428. return menu;
  429. };
  430. // Making local copy of index so that the lambda can use it, and not the changed value of 'i'.
  431. var qtyColumn = new Tuple<DynamicActionColumn, QuantityControl?>(null!, null);
  432. QuantityColumns[idx] = new DynamicTemplateColumn(row =>
  433. {
  434. var instance = LoadItem(row);
  435. var control = new QuantityControl(this, instance, idx);
  436. QuantityControls.Add(control);
  437. return control;
  438. })
  439. {
  440. HeaderText = "Qty.",
  441. Width = 60,
  442. ContextMenu = contextMenuFunc
  443. };
  444. CostColumns[idx] = new DynamicTextColumn(row =>
  445. {
  446. if(row is null)
  447. {
  448. return "Cost";
  449. }
  450. var instance = LoadItem(row);
  451. var qty = instance.GetQuantity(idx);
  452. var supplierProduct = GetSupplierProduct(instance, Suppliers[idx].ID);
  453. if(supplierProduct is not null)
  454. {
  455. return $"{qty * supplierProduct.CostPrice * instance.Multiplier:C2}";
  456. }
  457. else
  458. {
  459. return "";
  460. }
  461. })
  462. {
  463. HeaderText = "Cost",
  464. Width = 70,
  465. ContextMenu = contextMenuFunc,
  466. GetSummary = () =>
  467. {
  468. return new DynamicGridCustomSummary((rows) => Cost_Aggregate(idx, rows), "C2");
  469. }
  470. };
  471. ActionColumns.Add(QuantityColumns[idx]);
  472. ActionColumns.Add(CostColumns[idx]);
  473. }
  474. private double Cost_Aggregate(int supplierIdx, IEnumerable<CoreRow> rows)
  475. {
  476. return rows.Sum(row =>
  477. {
  478. var item = LoadItem(row);
  479. var supplierProduct = GetSupplierProduct(item, Suppliers[supplierIdx].ID);
  480. if (supplierProduct is not null)
  481. {
  482. var qty = item.GetQuantity(supplierIdx);
  483. return qty * item.Multiplier * supplierProduct.CostPrice;
  484. }
  485. else
  486. {
  487. return 0;
  488. }
  489. });
  490. }
  491. private void CreateSupplierProduct_Click(Tuple<ReservationManagementTreatmentPOItem, int> tuple)
  492. {
  493. var (item, supplierIdx) = tuple;
  494. var supplierProduct = new SupplierProduct();
  495. LookupFactory.DoLookup<SupplierProduct, Product, ProductLink>(supplierProduct, x => x.Product, item.TreatmentProduct.ID);
  496. supplierProduct.SupplierLink.CopyFrom(Suppliers[supplierIdx]);
  497. if (DynamicGridUtils.EditEntity(supplierProduct, customiseGrid: EditSupplierProductGrid))
  498. {
  499. SupplierProducts.Add(supplierProduct);
  500. InvalidateGrid();
  501. }
  502. }
  503. private static bool Matches(ReservationManagementTreatmentPOItem item, SupplierProduct supplierProduct, bool matchJob)
  504. {
  505. return item.TreatmentProduct.ID == supplierProduct.Product.ID
  506. && (!matchJob || item.JRI.Job.ID == supplierProduct.Job.ID);
  507. }
  508. private SupplierProduct? GetSupplierProduct(ReservationManagementTreatmentPOItem item)
  509. {
  510. var defaultSupplierProduct = SupplierProducts.FirstOrDefault(x => x.ID == item.Product.Supplier.ID);
  511. if(defaultSupplierProduct is not null && Matches(item, defaultSupplierProduct, false))
  512. {
  513. return defaultSupplierProduct;
  514. }
  515. else
  516. {
  517. return SupplierProducts.FirstOrDefault(x => Matches(item, x, true))
  518. ?? SupplierProducts.FirstOrDefault(x => Matches(item, x, false));
  519. }
  520. }
  521. private SupplierProduct? GetSupplierProduct(ReservationManagementTreatmentPOItem item, Guid supplierID)
  522. {
  523. return SupplierProducts.FirstOrDefault(x => x.SupplierLink.ID == supplierID && Matches(item, x, false));
  524. }
  525. //private double GetQuantity(SupplierProduct product)
  526. //{
  527. // var instance = ProductInstances.WithIndex().Where(x => x.Value.Product.ID == product.ID)
  528. //}
  529. private class CostAggregate : ISummaryAggregate
  530. {
  531. public double Sum { get; private set; }
  532. private int SupplierIndex;
  533. private ReservationManagementTreatmentOrderGrid Grid;
  534. public CostAggregate(int supplierIndex, ReservationManagementTreatmentOrderGrid grid)
  535. {
  536. SupplierIndex = supplierIndex;
  537. Grid = grid;
  538. }
  539. public Action<IEnumerable, string, PropertyDescriptor> CalculateAggregateFunc()
  540. {
  541. return AggregateFunc;
  542. }
  543. private void AggregateFunc(IEnumerable items, string property, PropertyDescriptor args)
  544. {
  545. if (items is IEnumerable<DataRowView> rows)
  546. {
  547. }
  548. else
  549. {
  550. Logger.Send(LogType.Error, "", $"Attempting to calculate aggregate on invalid data type '{items.GetType()}'.");
  551. }
  552. }
  553. }
  554. }