Files
qhmes/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs

1352 lines
53 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using HandyControl.Controls;
using HandyControl.Data;
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.Services.Service.MixerMaterialTareStrategy;
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<bool>
{
private readonly IRawMaterialEntryService _entryService;
/// <summary>供子页面(如独立新增页右侧面板)复用列表/详情查询能力。</summary>
protected IRawMaterialEntryService EntryService => _entryService;
private readonly IJeecgDictSyncService _dictSyncService;
private readonly IMixerMaterialService _mixerMaterialService;
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
private readonly ISupplierService _supplierService;
private bool _suppressTareStrategyRefresh;
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));
_ = RefreshAllRowsTareStrategyAsync();
// 新增模式自动生成条码/批次号
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 ? "新增原料入场记录" : "编辑原料入场记录";
/// <summary>已打印PrintFlag=="1"):总重只读、不可新增明细。</summary>
public bool IsPrinted => string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
public bool IsTotalWeightEditable => !IsPrinted;
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 DateTime? EntryTimeInput
{
get => Entry?.EntryTime;
set
{
if (Entry == null || Entry.EntryTime == value) return;
Entry.EntryTime = value;
RaisePropertyChanged(nameof(Entry));
_ = RefreshAllRowsTareStrategyAsync();
}
}
/// <summary>基础资料「托盘及皮重(合计)」= Σ 份数 × (包装物皮重 + 托盘重量)。</summary>
public double PalletTareTotalDisplay => SplitCodeDetails.Sum(row =>
{
var portions = row.Portions ?? 0;
var packaging = row.PackagingTare ?? 0d;
var pallet = row.PalletWeight ?? 0d;
return portions * (packaging + pallet);
});
public ObservableCollection<MesMixerMaterial> MaterialOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> TestStatusOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> PrintFlagOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> StockBalanceOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> IsSpecialAdoptionOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> StatusOptions { get; } = new();
public ObservableCollection<RawMaterialSplitDetailItem> SplitCodeDetails { get; } = new();
private bool _suspendEmbeddedPrintPreviewAirspace;
/// <summary>
/// WebView2 使用独立 HWNDAirspace会同窗体内浮在 WPF 元素之上,遮挡 HandyControl 内嵌 Dialog。
/// 原料入场独立页在弹窗期间通过绑定收起预览宿主;其它页面无 WebView2 时可忽略该属性。
/// </summary>
public bool SuspendEmbeddedPrintPreviewAirspace
{
get => _suspendEmbeddedPrintPreviewAirspace;
private set
{
if (!SetProperty(ref _suspendEmbeddedPrintPreviewAirspace, value))
{
return;
}
OnSuspendEmbeddedPrintPreviewAirspaceChanged();
}
}
/// <summary>子类在预览区可见性依赖此标志时,覆写以联动通知。</summary>
protected virtual void OnSuspendEmbeddedPrintPreviewAirspaceChanged()
{
}
/// <summary>在异步弹窗期间挂起右侧嵌入 WebView2避免遮挡模态内容。</summary>
protected async Task<TResult> SuspendEmbeddedPrintPreviewAirspaceWhileAsync<TResult>(Func<Task<TResult>> action)
{
SuspendEmbeddedPrintPreviewAirspace = true;
try
{
return await action();
}
finally
{
SuspendEmbeddedPrintPreviewAirspace = false;
}
}
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<RawMaterialSplitDetailItem> 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; }
/// <summary>拆码明细 - 库位选择命令弹出「库区选择」弹窗单选。CommandParameter 为当前行 RawMaterialSplitDetailItem。</summary>
public DelegateCommand<RawMaterialSplitDetailItem> OpenWarehouseAreaPickerCommand { get; }
/// <summary>拆码明细 - 手动选择皮重策略。</summary>
public DelegateCommand<RawMaterialSplitDetailItem> OpenTareStrategyPickerCommand { get; }
/// <summary>拆码明细 - 清除手动策略并按规则重新自动匹配。</summary>
public DelegateCommand<RawMaterialSplitDetailItem> ResetTareStrategyCommand { get; }
public RawMaterialEntryEditDialogViewModel(
IRawMaterialEntryService entryService,
IJeecgDictSyncService dictSyncService,
IMixerMaterialService mixerMaterialService,
IMixerMaterialTareStrategyService tareStrategyService,
ISupplierService supplierService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_entryService = entryService;
_dictSyncService = dictSyncService;
_mixerMaterialService = mixerMaterialService;
_tareStrategyService = tareStrategyService;
_supplierService = supplierService;
SaveCommand = new DelegateCommand(async () => await SaveAsync());
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
ResetCommand = new DelegateCommand(InitializeForAdd);
AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow, () => !IsPrinted);
RemoveSplitDetailCommand = new DelegateCommand<RawMaterialSplitDetailItem>(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);
OpenWarehouseAreaPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenWarehouseAreaPickerAsync(row));
OpenTareStrategyPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenTareStrategyPickerAsync(row));
ResetTareStrategyCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await ResetTareStrategyAsync(row));
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<string, string>("未检", "0"),
new KeyValuePair<string, string>("合格", "1"),
new KeyValuePair<string, string>("不合格", "2"),
});
PopulateOptions(TestStatusOptions, testStatusOpts, new[]
{
new KeyValuePair<string, string>("送样", "0"),
new KeyValuePair<string, string>("已批准", "1"),
});
PopulateOptions(PrintFlagOptions, printFlagOpts, new[]
{
new KeyValuePair<string, string>("未打印", "0"),
new KeyValuePair<string, string>("已打印", "1"),
});
PopulateOptions(StockBalanceOptions, ynOpts, new[]
{
new KeyValuePair<string, string>("否", "0"),
new KeyValuePair<string, string>("是", "1"),
});
PopulateOptions(IsSpecialAdoptionOptions, ynOpts, new[]
{
new KeyValuePair<string, string>("否", "0"),
new KeyValuePair<string, string>("是", "1"),
});
PopulateOptions(StatusOptions, statusOpts, Array.Empty<KeyValuePair<string, string>>());
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
}
catch
{
FillFallbackOptions();
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
}
}
private static void PopulateOptions(
ObservableCollection<KeyValuePair<string, string>> target,
IEnumerable<KeyValuePair<string, string>> items,
IEnumerable<KeyValuePair<string, string>> 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<KeyValuePair<string, string>>(), new[]
{
new KeyValuePair<string, string>("未检", "0"),
new KeyValuePair<string, string>("合格", "1"),
new KeyValuePair<string, string>("不合格", "2"),
});
PopulateOptions(TestStatusOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
{
new KeyValuePair<string, string>("送样", "0"),
new KeyValuePair<string, string>("已批准", "1"),
});
PopulateOptions(PrintFlagOptions, Array.Empty<KeyValuePair<string, string>>(), new[]
{
new KeyValuePair<string, string>("未打印", "0"),
new KeyValuePair<string, string>("已打印", "1"),
});
var ynDefault = new[] { new KeyValuePair<string, string>("否", "0"), new KeyValuePair<string, string>("是", "1") };
PopulateOptions(StockBalanceOptions, Array.Empty<KeyValuePair<string, string>>(), ynDefault);
PopulateOptions(IsSpecialAdoptionOptions, Array.Empty<KeyValuePair<string, string>>(), 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(EntryTimeInput));
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
RaisePropertyChanged(nameof(IsPrinted));
RaisePropertyChanged(nameof(IsTotalWeightEditable));
AddSplitDetailCommand.RaiseCanExecuteChanged();
}
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, PalletTareTotal = entry.PalletTareTotal,
TotalPortions = entry.TotalPortions,
PortionWeight = entry.PortionWeight,
PortionPackagingTare = entry.PortionPackagingTare,
PortionPalletWeight = entry.PortionPalletWeight,
PortionTareStrategyIds = entry.PortionTareStrategyIds,
PortionPackages = entry.PortionPackages,
PortionWarehouseLocations = entry.PortionWarehouseLocations,
PortionDetailIds = entry.PortionDetailIds,
PortionCardFlags = entry.PortionCardFlags,
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(EntryTimeInput));
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
RaisePropertyChanged(nameof(IsPrinted));
RaisePropertyChanged(nameof(IsTotalWeightEditable));
AddSplitDetailCommand.RaiseCanExecuteChanged();
}
/// <summary>校验并提交新增/编辑;不弹关闭、不执行独立页的 InitializeForAdd。成功时 Result=true。</summary>
protected async Task<bool> PersistEntryCoreAsync()
{
if (Entry == null) return false;
ApplySplitDetailsToEntry();
ApplyDefaultEntryStatusForAdd();
ApplyHiddenFieldDefaultsForAdd();
var missing = new List<string>();
if (string.IsNullOrWhiteSpace(Entry.MaterialId)) missing.Add("密炼物料");
if (string.IsNullOrWhiteSpace(Entry.BillNo)) missing.Add("榜单号");
if (string.IsNullOrWhiteSpace(Entry.UnloadOperator)) missing.Add("卸货人");
if (string.IsNullOrWhiteSpace(Entry.SupplierName)) missing.Add("供应商名称");
if (missing.Count > 0)
{
HandyControl.Controls.MessageBox.Warning($"以下必填项不能为空:{string.Join("", missing)}");
return false;
}
bool ok;
if (IsAddMode)
{
ok = await _entryService.AddAsync(Entry);
if (!ok) { HandyControl.Controls.MessageBox.Error("新增失败!"); return false; }
}
else
{
ok = await _entryService.EditAsync(Entry);
if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return false; }
}
Result = ok;
return true;
}
protected virtual async Task SaveAsync()
{
if (Entry == null) return;
try
{
if (!await PersistEntryCoreAsync()) return;
if (IsAddMode)
{
// 非模态轻提示,约 1 秒后自动消失(与独立页「保存并打印」时的体验一致)
Growl.Success(new GrowlInfo
{
Message = "保存成功",
ShowDateTime = false,
WaitTime = 1
});
if (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 SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() =>
HandyControl.Controls.Dialog.Show<WeightRecordPickerDialogView>()
.Initialize<WeightRecordPickerDialogViewModel>(vm =>
{
pickerVm = vm;
vm.Initialize(Entry?.BillNo);
})
.GetResultAsync<bool>());
}
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;
await ApplySupplierFromDisplayNameAsync(selected.SenderUnit);
// 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」,
// 用户仍可手动编辑;若净重为空则保留原值,避免误清空。
if (selected.NetWeight.HasValue)
{
var entered = selected.EnteredWeight ?? 0d;
var remaining = selected.NetWeight.Value - entered;
// 已入场重量可能因数据异常超过净重UI 不展示负数,强制夹到 0。
Entry.TotalWeight = Math.Round(Math.Max(0d, remaining), 2);
}
RaisePropertyChanged(nameof(Entry));
RaisePropertyChanged(nameof(TotalWeightInput));
RaisePropertyChanged(nameof(EntryTimeInput));
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
await RefreshAllRowsTareStrategyAsync();
}
/// <summary>
/// 按供应商全称/简称反查 ID榜单「发货单位」与手动填写的供应商名称均可用
/// </summary>
private async Task ApplySupplierFromDisplayNameAsync(string? displayName)
{
if (Entry == null || string.IsNullOrWhiteSpace(displayName))
{
return;
}
var supplier = await FindSupplierByDisplayNameAsync(displayName);
if (supplier == null)
{
return;
}
Entry.SupplierId = supplier.Id;
Entry.SupplierName = supplier.SupplierName
?? supplier.SupplierShortName
?? displayName.Trim();
RaisePropertyChanged(nameof(Entry));
}
private async Task<MesXslSupplier?> FindSupplierByDisplayNameAsync(string? displayName)
{
if (string.IsNullOrWhiteSpace(displayName))
{
return null;
}
var name = displayName.Trim();
try
{
var page = await _supplierService.PageAsync(1, 5000);
var exact = page.Records.FirstOrDefault(s =>
string.Equals(s.SupplierName?.Trim(), name, StringComparison.OrdinalIgnoreCase)
|| string.Equals(s.SupplierShortName?.Trim(), name, StringComparison.OrdinalIgnoreCase));
if (exact != null)
{
return exact;
}
return page.Records.FirstOrDefault(s =>
(s.SupplierName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase)
|| (s.SupplierShortName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase)
|| name.Contains(s.SupplierShortName ?? "", StringComparison.OrdinalIgnoreCase)
|| name.Contains(s.SupplierName ?? "", StringComparison.OrdinalIgnoreCase));
}
catch
{
return null;
}
}
/// <summary>
/// 皮重策略匹配前确保 SupplierId 已解析(榜单仅带出名称时补全 ID
/// </summary>
private async Task EnsureSupplierIdForTareMatchAsync()
{
if (Entry == null || !string.IsNullOrWhiteSpace(Entry.SupplierId))
{
return;
}
await ApplySupplierFromDisplayNameAsync(Entry.SupplierName);
}
/// <summary>
/// 弹出「库区选择」弹窗,把选中的 AreaName 写回当前明细行的 WarehouseLocation。
/// 入参 row被点击的拆码明细行为 null 直接 return防御性
/// </summary>
private async Task OpenWarehouseAreaPickerAsync(RawMaterialSplitDetailItem? row)
{
if (row == null)
{
return;
}
WarehouseAreaPickerDialogViewModel? pickerVm = null;
bool confirmed;
try
{
confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() =>
HandyControl.Controls.Dialog.Show<WarehouseAreaPickerDialogView>()
.Initialize<WarehouseAreaPickerDialogViewModel>(vm =>
{
pickerVm = vm;
vm.Initialize(row.WarehouseLocation);
})
.GetResultAsync<bool>());
}
catch (Exception ex)
{
// 保留 Debug 输出以便排查,但不阻断流程
System.Diagnostics.Debug.WriteLine($"[库位选择] 弹窗异常: {ex}");
return;
}
if (!confirmed || pickerVm?.SelectedRecord == null)
{
return;
}
// 仅回填库区名称(与后端 warehouse_location 字段保持单字符串语义)。
// 如未来需要严格关联库区 ID可在 RawMaterialSplitDetailItem 新增 WarehouseAreaId 一并保存。
row.WarehouseLocation = pickerVm.SelectedRecord.AreaName;
}
private async Task OpenMaterialPickerAsync()
{
RawMaterialEntryMaterialPickerDialogViewModel? pickerVm = null;
bool confirmed;
try
{
confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() =>
HandyControl.Controls.Dialog.Show<RawMaterialEntryMaterialPickerDialogView>()
.Initialize<RawMaterialEntryMaterialPickerDialogViewModel>(vm =>
{
pickerVm = vm;
vm.Initialize(Entry?.MaterialCode, Entry?.MaterialName);
})
.GetResultAsync<bool>());
}
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));
_ = RefreshAllRowsTareStrategyAsync();
}
private async Task OpenSupplierPickerAsync()
{
SupplierPickerDialogViewModel? pickerVm = null;
bool confirmed;
try
{
confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(() =>
HandyControl.Controls.Dialog.Show<SupplierPickerDialogView>()
.Initialize<SupplierPickerDialogViewModel>(vm => pickerVm = vm)
.GetResultAsync<bool>());
}
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));
await RefreshAllRowsTareStrategyAsync();
}
private void ClearSupplierSelection()
{
if (Entry == null) return;
Entry.SupplierId = null;
Entry.SupplierName = null;
RaisePropertyChanged(nameof(Entry));
_ = RefreshAllRowsTareStrategyAsync();
}
private async Task RefreshAllRowsTareStrategyAsync()
{
if (_suppressTareStrategyRefresh) return;
await EnsureSupplierIdForTareMatchAsync();
foreach (var row in SplitCodeDetails.ToList())
{
if (!row.IsManualTareStrategy)
await ApplyAutoTareStrategyToRowAsync(row);
}
}
private async Task ApplyAutoTareStrategyToRowAsync(RawMaterialSplitDetailItem row)
{
if (_suppressTareStrategyRefresh || row.IsManualTareStrategy || row.HasCard) return;
try
{
var strategies = await _tareStrategyService.GetAllForMatchAsync();
var match = MixerMaterialTareStrategyMatcher.PickBestMatch(
strategies,
Entry?.MaterialId,
Entry?.SupplierId,
Entry?.EntryTime,
row.PortionWeight);
ApplyTareStrategyToRow(row, match, manual: false);
}
catch
{
ApplyTareStrategyToRow(row, null, manual: false);
}
RaisePalletTareTotalChanged();
}
private void ApplyTareStrategyToRow(
RawMaterialSplitDetailItem row,
MesXslMixerMaterialTareStrategy? strategy,
bool manual)
{
if (strategy == null)
{
row.PackagingTare = 0d;
row.PalletWeight = 0d;
row.TareStrategyId = null;
row.TareStrategyDisplay = "未匹配(0)";
}
else
{
row.PackagingTare = strategy.TareWeight.HasValue ? (double)strategy.TareWeight.Value : 0d;
row.PalletWeight = strategy.PalletWeight.HasValue ? (double)strategy.PalletWeight.Value : 0d;
row.TareStrategyId = strategy.Id;
row.TareStrategyDisplay = BuildTareStrategyDisplay(strategy);
}
row.IsManualTareStrategy = manual && strategy != null;
}
private static string BuildTareStrategyDisplay(MesXslMixerMaterialTareStrategy strategy)
{
var spec = string.IsNullOrWhiteSpace(strategy.MaterialSpec) ? "-" : strategy.MaterialSpec;
var pkg = strategy.TareWeight?.ToString("0.##") ?? "0";
var pallet = strategy.PalletWeight?.ToString("0.##") ?? "0";
return $"规格:{spec} 包装:{pkg} 托盘:{pallet}";
}
private async Task OpenTareStrategyPickerAsync(RawMaterialSplitDetailItem? row)
{
if (row == null || row.HasCard) return;
if (string.IsNullOrWhiteSpace(Entry?.MaterialId))
{
HandyControl.Controls.MessageBox.Warning("请先选择密炼物料!");
return;
}
await EnsureSupplierIdForTareMatchAsync();
if (string.IsNullOrWhiteSpace(Entry?.SupplierId))
{
HandyControl.Controls.MessageBox.Warning("未能匹配到供应商档案,请手动选择供应商,或检查榜单发货单位是否与供应商简称/全称一致!");
return;
}
TareStrategyPickerDialogViewModel? pickerVm = null;
bool confirmed;
try
{
confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(async () =>
{
return await HandyControl.Controls.Dialog.Show<TareStrategyPickerDialogView>()
.Initialize<TareStrategyPickerDialogViewModel>(vm =>
{
pickerVm = vm;
vm.Initialize(Entry!.MaterialId, Entry.SupplierId, Entry.EntryTime, row.PortionWeight, row.TareStrategyId);
})
.GetResultAsync<bool>();
});
}
catch { return; }
if (!confirmed) return;
if (pickerVm?.SelectedStrategy == null)
{
row.IsManualTareStrategy = false;
await ApplyAutoTareStrategyToRowAsync(row);
return;
}
ApplyTareStrategyToRow(row, pickerVm.SelectedStrategy, manual: true);
RaisePalletTareTotalChanged();
}
private async Task ResetTareStrategyAsync(RawMaterialSplitDetailItem? row)
{
if (row == null || row.HasCard) return;
row.IsManualTareStrategy = false;
await ApplyAutoTareStrategyToRowAsync(row);
}
private void RaisePalletTareTotalChanged() => RaisePropertyChanged(nameof(PalletTareTotalDisplay));
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";
}
/// <summary>
/// 新增模式下为已在前端隐藏的字段补充默认值,确保保存到后端时不为空:
/// 检测结果=未检、打印标记=未打印、入库结存=否。
/// 字典就绪时取字典 code未就绪时回退到约定的常用 code。
/// </summary>
protected void ApplyHiddenFieldDefaultsForAdd()
{
if (!IsAddMode || Entry == null)
{
return;
}
if (string.IsNullOrWhiteSpace(Entry.TestResult))
{
Entry.TestResult = ResolveDefaultOptionValue(TestResultOptions, "未检", "0");
}
if (string.IsNullOrWhiteSpace(Entry.PrintFlag))
{
Entry.PrintFlag = ResolveDefaultOptionValue(PrintFlagOptions, "未打印", "0");
}
if (string.IsNullOrWhiteSpace(Entry.StockBalance))
{
Entry.StockBalance = ResolveDefaultOptionValue(StockBalanceOptions, "否", "0");
}
RaisePropertyChanged(nameof(Entry));
}
/// <summary>在字典选项中按 Key显示标签查找对应 Value实际 code找不到则回退到 fallback。</summary>
private static string ResolveDefaultOptionValue(
ObservableCollection<KeyValuePair<string, string>> 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()
{
_suppressTareStrategyRefresh = true;
try
{
SplitCodeDetails.Clear();
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
var packagingTareArr = SplitJoinedValues(Entry?.PortionPackagingTare);
var palletWeightArr = SplitJoinedValues(Entry?.PortionPalletWeight);
var strategyIdsArr = SplitJoinedValues(Entry?.PortionTareStrategyIds);
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations);
var idsArr = SplitJoinedValues(Entry?.PortionDetailIds);
var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags);
var fallbackToPrintFlag = cardFlagsArr.Length == 0
&& string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length),
Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length)));
for (var i = 0; i < rowCount; i++)
{
var locationFromArr = GetAt(locationsArr, i);
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
? Entry?.WarehouseLocation
: locationFromArr;
var strategyId = GetAt(strategyIdsArr, i);
var item = new RawMaterialSplitDetailItem
{
Portions = TryParseInt(GetAt(portionsArr, i)),
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
WarehouseLocation = locationFallback,
PackagingTare = TryParseDouble(GetAt(packagingTareArr, i)) ?? 0d,
PalletWeight = TryParseDouble(GetAt(palletWeightArr, i)) ?? 0d,
TareStrategyId = strategyId,
IsManualTareStrategy = !string.IsNullOrWhiteSpace(strategyId),
};
item.TareStrategyDisplay = string.IsNullOrWhiteSpace(strategyId)
? "未匹配(0)"
: $"策略ID:{strategyId}";
var existingId = GetAt(idsArr, i);
if (!string.IsNullOrWhiteSpace(existingId))
item.Id = existingId;
var flagAt = GetAt(cardFlagsArr, i);
if (!string.IsNullOrWhiteSpace(flagAt))
item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal);
else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId))
item.HasCard = true;
SplitCodeDetails.Add(item);
}
RaisePropertyChanged(nameof(SplitCodeTableHeight));
RaisePalletTareTotalChanged();
}
finally
{
_suppressTareStrategyRefresh = false;
}
}
private static string[] SplitJoinedValues(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return Array.Empty<string>();
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()
{
var row = new RawMaterialSplitDetailItem();
SplitCodeDetails.Add(row);
RaisePropertyChanged(nameof(SplitCodeTableHeight));
_ = ApplyAutoTareStrategyToRowAsync(row);
}
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<RawMaterialSplitDetailItem>())
{
oldItem.PropertyChanged -= OnSplitDetailItemPropertyChanged;
}
}
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems.OfType<RawMaterialSplitDetailItem>())
{
newItem.PropertyChanged += OnSplitDetailItemPropertyChanged;
}
}
RaiseSplitDisplayPropertyChanged();
RaisePropertyChanged(nameof(SplitCodeTableHeight));
}
private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// 用户修改某行的「份数」时:仅当该行「每份重量」为空才按公式自动计算
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions)
&& sender is RawMaterialSplitDetailItem row)
{
RecalculatePortionWeightForRow(row);
}
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.PortionWeight)
&& sender is RawMaterialSplitDetailItem weightRow)
{
// 清空每份重量后,若份数已填则立即按公式重算(配合 PortionWeightText 空值绑定)
if (!weightRow.PortionWeight.HasValue)
{
RecalculatePortionWeightForRow(weightRow);
}
if (!weightRow.IsManualTareStrategy && !weightRow.HasCard)
{
_ = ApplyAutoTareStrategyToRowAsync(weightRow);
}
}
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions)
or nameof(RawMaterialSplitDetailItem.PortionWeight)
or nameof(RawMaterialSplitDetailItem.PortionPackages)
or nameof(RawMaterialSplitDetailItem.PackagingTare)
or nameof(RawMaterialSplitDetailItem.PalletWeight))
{
RaiseSplitDisplayPropertyChanged();
}
}
/// <summary>
/// 拆码明细某行的「份数」变化时,仅当该行「每份重量」为空时按公式重算:
/// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数
/// — 已填写「每份重量」后再改份数,不覆盖用户输入;
/// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过;
/// — 结果四舍五入到两位小数。
/// </summary>
private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row)
{
if (row.PortionWeight.HasValue)
{
return;
}
if (Entry?.TotalWeight is not { } total || total <= 0d)
{
return;
}
if (row.Portions is not { } portions || portions <= 0)
{
return;
}
var otherSum = 0d;
foreach (var other in SplitCodeDetails)
{
if (ReferenceEquals(other, row)) continue;
if (other.Portions is not { } op || other.PortionWeight is not { } ow) continue;
otherSum += op * ow;
}
var remaining = total - otherSum;
if (remaining <= 0d)
{
return;
}
row.PortionWeight = Math.Round(remaining / portions, 2);
}
private void RaiseSplitDisplayPropertyChanged()
{
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
RaisePalletTareTotalChanged();
}
private string JoinSplitValue(Func<RawMaterialSplitDetailItem, string?> 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);
}
/// <summary>
/// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数 / 库位 / 行 ID」按 "x/y/z/" 拼接后持久化到 Entry。
/// 明细行的「库位」属于行级数据(用于后续生成原材料卡片时区分库位),与 Entry.WarehouseLocation
/// (基础资料整票级单值)独立保存,通过 portion_warehouse_locations 字段持久化。
/// 子类(如 OperationViewModel 的「重新拆码」)需要直接调用,因此声明为 protected。
/// </summary>
protected 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.PortionPackagingTare = JoinSplitValue(it => FormatNullableDecimal(it.PackagingTare), true);
Entry.PortionPalletWeight = JoinSplitValue(it => FormatNullableDecimal(it.PalletWeight), true);
Entry.PortionTareStrategyIds = JoinSplitValue(
it => it.IsManualTareStrategy && !string.IsNullOrWhiteSpace(it.TareStrategyId) ? it.TareStrategyId : null,
true);
Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true);
Entry.PortionWarehouseLocations = JoinSplitValue(it => string.IsNullOrWhiteSpace(it.WarehouseLocation) ? null : it.WarehouseLocation.Trim(), true);
// 持久化每行 GUID行序与上方四个字段对齐JoinSplitValue 会过滤空,但 Id 在构造时即生成不会为空。
Entry.PortionDetailIds = JoinSplitValue(it => it.Id, true);
// 持久化每行的「已生成卡片」标志1=已生成 0=未生成),
// 用于解决「保存后新增行被误判为已打印」的问题:
// 「生成原材料卡片」只对 HasCard=false 的行加卡 → 必须把真实的 HasCard 持久化,
// 否则重新加载后无法区分「已生成」与「保存后新增的未生成行」。
Entry.PortionCardFlags = JoinSplitValue(it => it.HasCard ? "1" : "0", true);
Entry.PalletTareTotal = PalletTareTotalDisplay;
}
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
{
/// <summary>
/// 拆码明细行 IDGUID构造时生成。前端隐藏
/// 「生成原材料卡片」时由桌面端写入对应 MesXslRawMaterialCard.SplitDetailId
/// 「重新拆码」时按入场记录的 PortionDetailIds 反查批删关联卡片。
/// 编辑回填时若 portion_detail_ids 已有该位置的值,会覆盖默认值,确保跨次保持稳定。
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString("N");
private int? _portions;
public int? Portions
{
get => _portions;
set => SetProperty(ref _portions, value);
}
private double? _portionWeight;
public double? PortionWeight
{
get => _portionWeight;
set
{
if (SetProperty(ref _portionWeight, value))
{
RaisePropertyChanged(nameof(PortionWeightText));
}
}
}
/// <summary>
/// 每份重量文本(供 TextBox 绑定;空字符串表示未填写,避免 double? 直接绑定无法清空)。
/// </summary>
public string PortionWeightText
{
get => _portionWeight.HasValue
? _portionWeight.Value.ToString("0.##", CultureInfo.InvariantCulture)
: string.Empty;
set
{
var text = value?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(text))
{
PortionWeight = null;
return;
}
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
{
PortionWeight = parsed;
}
}
}
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);
}
private double? _packagingTare;
public double? PackagingTare
{
get => _packagingTare;
set => SetProperty(ref _packagingTare, value);
}
private double? _palletWeight;
public double? PalletWeight
{
get => _palletWeight;
set => SetProperty(ref _palletWeight, value);
}
private string? _tareStrategyId;
public string? TareStrategyId
{
get => _tareStrategyId;
set => SetProperty(ref _tareStrategyId, value);
}
private string? _tareStrategyDisplay = "未匹配(0)";
public string? TareStrategyDisplay
{
get => _tareStrategyDisplay;
set => SetProperty(ref _tareStrategyDisplay, value);
}
private bool _isManualTareStrategy;
/// <summary>用户手动选择皮重策略后为 true自动匹配不再覆盖。</summary>
public bool IsManualTareStrategy
{
get => _isManualTareStrategy;
set => SetProperty(ref _isManualTareStrategy, value);
}
/// <summary>
/// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。
/// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断;
/// 「生成原材料卡片」成功后由 VM 置为 true「重新拆码」清空集合时随之失效。
/// true → 行内输入项 / 删除 全部锁定,避免与已生成卡片数据脱节;
/// false → 该行参与下次「生成原材料卡片」(继续拆码 + 续号生成)。
/// </summary>
private bool _hasCard;
public bool HasCard
{
get => _hasCard;
set => SetProperty(ref _hasCard, value);
}
}