using HandyControl.Controls; using HandyControl.Tools.Extension; using System.Collections.Specialized; using System.ComponentModel; using System.Collections.ObjectModel; using System.Globalization; using YY.Admin.Core; using YY.Admin.Core.Entity; using YY.Admin.Core.Services; using YY.Admin.Services.Service; using YY.Admin.Views.RawMaterialEntry; using YY.Admin.ViewModels.WeightRecord; using YY.Admin.Views.WeightRecord; namespace YY.Admin.ViewModels.RawMaterialEntry; public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResultable { private readonly IRawMaterialEntryService _entryService; /// 供子页面(如独立新增页右侧面板)复用列表/详情查询能力。 protected IRawMaterialEntryService EntryService => _entryService; private readonly IJeecgDictSyncService _dictSyncService; private readonly IMixerMaterialService _mixerMaterialService; // 加载完物料后用于回填 Edit 模式选中项 protected string? _pendingMaterialId; private MesXslRawMaterialEntry? _entry; public MesXslRawMaterialEntry? Entry { get => _entry; set => SetProperty(ref _entry, value); } protected MesMixerMaterial? _selectedMaterial; public MesMixerMaterial? SelectedMaterial { get => _selectedMaterial; set { if (!SetProperty(ref _selectedMaterial, value) || value == null || Entry == null) return; Entry.MaterialId = value.Id; Entry.MaterialCode = value.MaterialCode; Entry.MaterialName = value.MaterialName; Entry.ManufacturerMaterialName = value.AliasName; RecalculateShelfLife(value); RaisePropertyChanged(nameof(Entry)); RaisePropertyChanged(nameof(SelectedMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMaterial)); // 新增模式自动生成条码/批次号 if (IsAddMode && !string.IsNullOrEmpty(value.MaterialCode)) _ = AutoGenerateBarcodeAsync(value.MaterialCode); } } private bool _isGenerating; public bool IsGenerating { get => _isGenerating; set => SetProperty(ref _isGenerating, value); } public bool IsAddMode => string.IsNullOrWhiteSpace(Entry?.Id); public string DialogTitle => IsAddMode ? "新增原料入场记录" : "编辑原料入场记录"; public string SelectedMaterialDisplay => _selectedMaterial == null ? "请选择密炼物料" : $"[{_selectedMaterial.MaterialCode}] {_selectedMaterial.MaterialName}"; public bool HasSelectedMaterial => _selectedMaterial != null; public string? IsSpecialAdoptionValue { get => Entry?.IsSpecialAdoption; set { if (Entry == null || Entry.IsSpecialAdoption == value) { return; } Entry.IsSpecialAdoption = value; if (!string.Equals(value, "1", StringComparison.Ordinal)) { Entry.SpecialAdoptionOperator = null; Entry.SpecialAdoptionTime = null; Entry.SpecialAdoptionReason = null; } RaisePropertyChanged(nameof(Entry)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); } } public double? TotalWeightInput { get => Entry?.TotalWeight; set { if (Entry == null || Entry.TotalWeight == value) return; Entry.TotalWeight = value; RaisePropertyChanged(nameof(Entry)); } } public ObservableCollection MaterialOptions { get; } = new(); public ObservableCollection> TestResultOptions { get; } = new(); public ObservableCollection> TestStatusOptions { get; } = new(); public ObservableCollection> PrintFlagOptions { get; } = new(); public ObservableCollection> StockBalanceOptions { get; } = new(); public ObservableCollection> IsSpecialAdoptionOptions { get; } = new(); public ObservableCollection> StatusOptions { get; } = new(); public ObservableCollection SplitCodeDetails { get; } = new(); public double SplitCodeTableHeight => CalculateSplitCodeTableHeight(); public string SplitTotalPortionsDisplay => JoinSplitValue(item => item.Portions?.ToString(CultureInfo.InvariantCulture), true); public string SplitPortionWeightDisplay => JoinSplitValue(item => FormatNullableDecimal(item.PortionWeight), true); public string SplitPortionPackagesDisplay => JoinSplitValue(item => item.PortionPackages?.ToString(CultureInfo.InvariantCulture), true); private bool _result; public bool Result { get => _result; set => SetProperty(ref _result, value); } public Action? CloseAction { get; set; } public DelegateCommand SaveCommand { get; } public DelegateCommand CancelCommand { get; } public DelegateCommand ResetCommand { get; } public DelegateCommand AddSplitDetailCommand { get; } public DelegateCommand RemoveSplitDetailCommand { get; } public DelegateCommand OpenMaterialPickerCommand { get; } public DelegateCommand ClearMaterialCommand { get; } public DelegateCommand OpenWeightRecordPickerCommand { get; } public DelegateCommand ClearWeightRecordCommand { get; } public DelegateCommand OpenSupplierPickerCommand { get; } public DelegateCommand ClearSupplierCommand { get; } public RawMaterialEntryEditDialogViewModel( IRawMaterialEntryService entryService, IJeecgDictSyncService dictSyncService, IMixerMaterialService mixerMaterialService, IContainerExtension container, IRegionManager regionManager) : base(container, regionManager) { _entryService = entryService; _dictSyncService = dictSyncService; _mixerMaterialService = mixerMaterialService; SaveCommand = new DelegateCommand(async () => await SaveAsync()); CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); ResetCommand = new DelegateCommand(InitializeForAdd); AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow); RemoveSplitDetailCommand = new DelegateCommand(RemoveSplitDetailRow); OpenMaterialPickerCommand = new DelegateCommand(async () => await OpenMaterialPickerAsync()); ClearMaterialCommand = new DelegateCommand(ClearMaterialSelection); OpenWeightRecordPickerCommand = new DelegateCommand(async () => await OpenWeightRecordPickerAsync()); ClearWeightRecordCommand = new DelegateCommand(ClearWeightRecordSelection); OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync()); ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection); SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged; _ = LoadAllAsync(); } private async Task LoadAllAsync() { await Task.WhenAll(LoadDictOptionsAsync(), LoadMaterialOptionsAsync()); } protected async Task LoadMaterialOptionsAsync() { try { // 每次打开弹窗都主动拉一次,确保直接写库的数据也能同步到 await _mixerMaterialService.SyncFromRemoteAsync(); var result = await _mixerMaterialService.PageAsync(1, 2000); MaterialOptions.Clear(); foreach (var m in result.Records) MaterialOptions.Add(m); // Edit 模式:物料加载完后回填选中项 if (_pendingMaterialId != null) { var match = MaterialOptions.FirstOrDefault(m => string.Equals(m.Id, _pendingMaterialId, StringComparison.OrdinalIgnoreCase)); if (match != null) SelectedMaterial = match; _pendingMaterialId = null; } } catch { /* 无法加载物料列表时不阻断表单 */ } } private async Task LoadDictOptionsAsync() { try { var testResultOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_result"); var testStatusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_test_status"); var printFlagOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_print_flag"); var ynOpts = await _dictSyncService.GetDictOptionsAsync("yn"); var statusOpts = await _dictSyncService.GetDictOptionsAsync("xslmes_entry_status"); PopulateOptions(TestResultOptions, testResultOpts, new[] { new KeyValuePair("未检", "0"), new KeyValuePair("合格", "1"), new KeyValuePair("不合格", "2"), }); PopulateOptions(TestStatusOptions, testStatusOpts, new[] { new KeyValuePair("送样", "0"), new KeyValuePair("已批准", "1"), }); PopulateOptions(PrintFlagOptions, printFlagOpts, new[] { new KeyValuePair("未打印", "0"), new KeyValuePair("已打印", "1"), }); PopulateOptions(StockBalanceOptions, ynOpts, new[] { new KeyValuePair("否", "0"), new KeyValuePair("是", "1"), }); PopulateOptions(IsSpecialAdoptionOptions, ynOpts, new[] { new KeyValuePair("否", "0"), new KeyValuePair("是", "1"), }); PopulateOptions(StatusOptions, statusOpts, Array.Empty>()); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); } catch { FillFallbackOptions(); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); } } private static void PopulateOptions( ObservableCollection> target, IEnumerable> items, IEnumerable> fallback) { target.Clear(); var list = items.ToList(); foreach (var item in list.Count > 0 ? list : fallback.ToList()) target.Add(item); } private void FillFallbackOptions() { PopulateOptions(TestResultOptions, Array.Empty>(), new[] { new KeyValuePair("未检", "0"), new KeyValuePair("合格", "1"), new KeyValuePair("不合格", "2"), }); PopulateOptions(TestStatusOptions, Array.Empty>(), new[] { new KeyValuePair("送样", "0"), new KeyValuePair("已批准", "1"), }); PopulateOptions(PrintFlagOptions, Array.Empty>(), new[] { new KeyValuePair("未打印", "0"), new KeyValuePair("已打印", "1"), }); var ynDefault = new[] { new KeyValuePair("否", "0"), new KeyValuePair("是", "1") }; PopulateOptions(StockBalanceOptions, Array.Empty>(), ynDefault); PopulateOptions(IsSpecialAdoptionOptions, Array.Empty>(), ynDefault); } protected async Task AutoGenerateBarcodeAsync(string materialCode) { IsGenerating = true; try { var code = await _entryService.GenerateBarcodeAsync(materialCode); if (!string.IsNullOrEmpty(code) && Entry != null) { Entry.Barcode = code; Entry.BatchNo = code; RaisePropertyChanged(nameof(Entry)); } } finally { IsGenerating = false; } } public virtual void InitializeForAdd() { _selectedMaterial = null; RaisePropertyChanged(nameof(SelectedMaterial)); RaisePropertyChanged(nameof(SelectedMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMaterial)); Entry = new MesXslRawMaterialEntry { EntryTime = DateTime.Now, IsSpecialAdoption = "0" }; InitializeSplitCodeDetailsFromEntry(); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); RaisePropertyChanged(nameof(IsAddMode)); RaisePropertyChanged(nameof(DialogTitle)); RaisePropertyChanged(nameof(TotalWeightInput)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); } public void InitializeForEdit(MesXslRawMaterialEntry entry) { Entry = new MesXslRawMaterialEntry { Id = entry.Id, Barcode = entry.Barcode, BatchNo = entry.BatchNo, EntryTime = entry.EntryTime, WeightRecordId = entry.WeightRecordId, BillNo = entry.BillNo, MaterialId = entry.MaterialId, MaterialCode = entry.MaterialCode, MaterialName = entry.MaterialName, SupplyCustomer = entry.SupplyCustomer, SupplierId = entry.SupplierId, SupplierName = entry.SupplierName, ManufacturerMaterialName = entry.ManufacturerMaterialName, ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, TotalPortions = entry.TotalPortions, PortionWeight = entry.PortionWeight, PortionPackages = entry.PortionPackages, TestResult = entry.TestResult, TestStatus = entry.TestStatus, PrintFlag = entry.PrintFlag, StockBalance = entry.StockBalance, WarehouseLocation = entry.WarehouseLocation, UnloadOperator = entry.UnloadOperator, IsSpecialAdoption = entry.IsSpecialAdoption, SpecialAdoptionOperator = entry.SpecialAdoptionOperator, SpecialAdoptionTime = entry.SpecialAdoptionTime, SpecialAdoptionReason = entry.SpecialAdoptionReason, Status = entry.Status, Remark = entry.Remark, TenantId = entry.TenantId, }; InitializeSplitCodeDetailsFromEntry(); // 若物料列表已加载则直接回填,否则记录 pending 等加载完后回填 if (MaterialOptions.Count > 0) { _selectedMaterial = MaterialOptions.FirstOrDefault(m => string.Equals(m.Id, entry.MaterialId, StringComparison.OrdinalIgnoreCase)); RaisePropertyChanged(nameof(SelectedMaterial)); RaisePropertyChanged(nameof(SelectedMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMaterial)); } else { _pendingMaterialId = entry.MaterialId; } RaisePropertyChanged(nameof(IsAddMode)); RaisePropertyChanged(nameof(DialogTitle)); RaisePropertyChanged(nameof(TotalWeightInput)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); } protected virtual async Task SaveAsync() { if (Entry == null) return; try { ApplySplitDetailsToEntry(); ApplyDefaultEntryStatusForAdd(); ApplyHiddenFieldDefaultsForAdd(); bool ok; if (IsAddMode) { ok = await _entryService.AddAsync(Entry); if (ok) HandyControl.Controls.MessageBox.Success("新增成功!"); else { HandyControl.Controls.MessageBox.Error("新增失败!"); return; } } else { ok = await _entryService.EditAsync(Entry); if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return; } } Result = ok; if (IsAddMode && CloseAction == null) { // 独立新增页面:保存成功后自动清空表单,便于连续录入 InitializeForAdd(); return; } CloseAction?.Invoke(); } catch (Exception ex) { HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}"); } } private async Task OpenWeightRecordPickerAsync() { WeightRecordPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; vm.Initialize(Entry?.BillNo); }) .GetResultAsync(); } catch { return; } if (!confirmed || pickerVm?.SelectedRecord == null || Entry == null) { return; } var selected = pickerVm.SelectedRecord; Entry.WeightRecordId = selected.Id; Entry.BillNo = selected.BillNo; Entry.SupplierName = selected.SenderUnit; Entry.SupplierId = null; RaisePropertyChanged(nameof(Entry)); } private async Task OpenMaterialPickerAsync() { RawMaterialEntryMaterialPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await HandyControl.Controls.Dialog.Show() .Initialize(vm => { pickerVm = vm; vm.Initialize(Entry?.MaterialCode, Entry?.MaterialName); }) .GetResultAsync(); } catch { return; } if (!confirmed || pickerVm?.SelectedMaterial == null) { return; } SelectedMaterial = pickerVm.SelectedMaterial; } private void ClearMaterialSelection() { _selectedMaterial = null; RaisePropertyChanged(nameof(SelectedMaterial)); RaisePropertyChanged(nameof(SelectedMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMaterial)); if (Entry == null) { return; } Entry.MaterialId = null; Entry.MaterialCode = null; Entry.MaterialName = null; Entry.ManufacturerMaterialName = null; Entry.ShelfLife = null; RaisePropertyChanged(nameof(Entry)); } private void ClearWeightRecordSelection() { if (Entry == null) { return; } Entry.WeightRecordId = null; Entry.BillNo = null; Entry.SupplierId = null; Entry.SupplierName = null; RaisePropertyChanged(nameof(Entry)); } private async Task OpenSupplierPickerAsync() { SupplierPickerDialogViewModel? pickerVm = null; bool confirmed; try { confirmed = await HandyControl.Controls.Dialog.Show() .Initialize(vm => pickerVm = vm) .GetResultAsync(); } catch { return; } if (!confirmed || pickerVm?.SelectedSupplier == null || Entry == null) return; var selected = pickerVm.SelectedSupplier; Entry.SupplierId = selected.Id; Entry.SupplierName = selected.SupplierName; RaisePropertyChanged(nameof(Entry)); } private void ClearSupplierSelection() { if (Entry == null) return; Entry.SupplierId = null; Entry.SupplierName = null; RaisePropertyChanged(nameof(Entry)); } private void RecalculateShelfLife(MesMixerMaterial? material) { if (Entry == null || material?.ShelfLifeDays == null || material.ShelfLifeDays <= 0) { return; } Entry.ShelfLife = DateTime.Now.Date.AddDays(material.ShelfLifeDays.Value).ToString("yyyy-MM-dd"); } protected void ApplyDefaultEntryStatusForAdd() { if (!IsAddMode || Entry == null || !string.IsNullOrWhiteSpace(Entry.Status)) { return; } var pending = StatusOptions.FirstOrDefault(x => string.Equals(x.Key?.Trim(), "待处理", StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(pending.Value)) { Entry.Status = pending.Value; return; } // 字典未就绪时使用常见默认值 Entry.Status = "0"; } /// /// 新增模式下为已在前端隐藏的字段补充默认值,确保保存到后端时不为空: /// 检测结果=未检、检测状态=送样、打印标记=未打印、入库结存=否。 /// 字典就绪时取字典 code,未就绪时回退到约定的常用 code。 /// protected void ApplyHiddenFieldDefaultsForAdd() { if (!IsAddMode || Entry == null) { return; } if (string.IsNullOrWhiteSpace(Entry.TestResult)) { Entry.TestResult = ResolveDefaultOptionValue(TestResultOptions, "未检", "0"); } if (string.IsNullOrWhiteSpace(Entry.TestStatus)) { Entry.TestStatus = ResolveDefaultOptionValue(TestStatusOptions, "送样", "0"); } if (string.IsNullOrWhiteSpace(Entry.PrintFlag)) { Entry.PrintFlag = ResolveDefaultOptionValue(PrintFlagOptions, "未打印", "0"); } if (string.IsNullOrWhiteSpace(Entry.StockBalance)) { Entry.StockBalance = ResolveDefaultOptionValue(StockBalanceOptions, "否", "0"); } RaisePropertyChanged(nameof(Entry)); } /// 在字典选项中按 Key(显示标签)查找对应 Value(实际 code),找不到则回退到 fallback。 private static string ResolveDefaultOptionValue( ObservableCollection> options, string keyLabel, string fallbackValue) { var match = options.FirstOrDefault(x => string.Equals(x.Key?.Trim(), keyLabel, StringComparison.OrdinalIgnoreCase)); return !string.IsNullOrWhiteSpace(match.Value) ? match.Value : fallbackValue; } protected void InitializeSplitCodeDetailsFromEntry() { SplitCodeDetails.Clear(); // 三个字段是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表 var portionsArr = SplitJoinedValues(Entry?.TotalPortions); var weightArr = SplitJoinedValues(Entry?.PortionWeight); var packagesArr = SplitJoinedValues(Entry?.PortionPackages); var rowCount = Math.Max(1, Math.Max(portionsArr.Length, Math.Max(weightArr.Length, packagesArr.Length))); for (var i = 0; i < rowCount; i++) { SplitCodeDetails.Add(new RawMaterialSplitDetailItem { Portions = TryParseInt(GetAt(portionsArr, i)), PortionWeight = TryParseDouble(GetAt(weightArr, i)), PortionPackages = TryParseInt(GetAt(packagesArr, i)), // 库位是单值字段,仅赋给首行 WarehouseLocation = i == 0 ? Entry?.WarehouseLocation : null, }); } RaisePropertyChanged(nameof(SplitCodeTableHeight)); } private static string[] SplitJoinedValues(string? value) { if (string.IsNullOrWhiteSpace(value)) return Array.Empty(); return value .Split('/', StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()) .Where(x => x.Length > 0) .ToArray(); } private static string? GetAt(string[] arr, int index) => index < arr.Length ? arr[index] : null; private static int? TryParseInt(string? text) => int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v : null; private static double? TryParseDouble(string? text) => double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : null; private void AddSplitDetailRow() { SplitCodeDetails.Add(new RawMaterialSplitDetailItem()); RaisePropertyChanged(nameof(SplitCodeTableHeight)); } private void RemoveSplitDetailRow(RawMaterialSplitDetailItem? item) { if (item == null) { return; } SplitCodeDetails.Remove(item); if (SplitCodeDetails.Count == 0) { SplitCodeDetails.Add(new RawMaterialSplitDetailItem()); } RaisePropertyChanged(nameof(SplitCodeTableHeight)); } private void OnSplitCodeDetailsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) { foreach (var oldItem in e.OldItems.OfType()) { oldItem.PropertyChanged -= OnSplitDetailItemPropertyChanged; } } if (e.NewItems != null) { foreach (var newItem in e.NewItems.OfType()) { newItem.PropertyChanged += OnSplitDetailItemPropertyChanged; } } RaiseSplitDisplayPropertyChanged(); RaisePropertyChanged(nameof(SplitCodeTableHeight)); } private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions) or nameof(RawMaterialSplitDetailItem.PortionWeight) or nameof(RawMaterialSplitDetailItem.PortionPackages)) { RaiseSplitDisplayPropertyChanged(); } } private void RaiseSplitDisplayPropertyChanged() { RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); } private string JoinSplitValue(Func selector, bool withTrailingSlash) { var values = SplitCodeDetails .Select(selector) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!.Trim()) .ToList(); if (values.Count == 0) { return string.Empty; } var joined = string.Join("/", values); return withTrailingSlash ? $"{joined}/" : joined; } private static string? FormatNullableDecimal(double? value) { if (!value.HasValue) { return null; } return value.Value.ToString("0.##", CultureInfo.InvariantCulture); } /// /// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数」按 "x/y/z/" 拼接后持久化到 Entry, /// 库位仍取首行(业务上库位是单值)。与 SplitTotalPortionsDisplay 等只读展示属性同规则。 /// private void ApplySplitDetailsToEntry() { if (Entry == null) return; Entry.TotalPortions = JoinSplitValue(it => it.Portions?.ToString(CultureInfo.InvariantCulture), true); Entry.PortionWeight = JoinSplitValue(it => FormatNullableDecimal(it.PortionWeight), true); Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true); if (SplitCodeDetails.Count > 0) { Entry.WarehouseLocation = SplitCodeDetails[0].WarehouseLocation; } } private double CalculateSplitCodeTableHeight() { // 与拆码明细 XAML 行高保持一致:表头 40px、数据行 44px const double headerHeight = 40d; const double rowHeight = 44d; const double framePadding = 16d; const double minRows = 1d; const double maxRows = 6d; var rowCount = Math.Clamp(SplitCodeDetails.Count, (int)minRows, (int)maxRows); return headerHeight + rowCount * rowHeight + framePadding; } } public class RawMaterialSplitDetailItem : BindableBase { private int? _portions; public int? Portions { get => _portions; set => SetProperty(ref _portions, value); } private double? _portionWeight; public double? PortionWeight { get => _portionWeight; set => SetProperty(ref _portionWeight, value); } private int? _portionPackages; public int? PortionPackages { get => _portionPackages; set => SetProperty(ref _portionPackages, value); } private string? _warehouseLocation; public string? WarehouseLocation { get => _warehouseLocation; set => SetProperty(ref _warehouseLocation, value); } }