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

1017 lines
41 KiB
C#
Raw Normal View History

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.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;
// 加载完物料后用于回填 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 ? "新增原料入场记录" : "编辑原料入场记录";
/// <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 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; }
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, () => !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));
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(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, TotalPortions = entry.TotalPortions,
PortionWeight = entry.PortionWeight, 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(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;
// 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」,
// 用户仍可手动编辑;若净重为空则保留原值,避免误清空。
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));
}
/// <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));
}
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));
}
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";
}
/// <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()
{
SplitCodeDetails.Clear();
// 六个字段都是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations);
var idsArr = SplitJoinedValues(Entry?.PortionDetailIds);
var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags);
// 历史记录可能没存 PortionCardFlags用 print_flag 推断(与原行为兼容)
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);
// 兼容历史数据:明细库位拼接字段为空时,首行回退到 Entry.WarehouseLocation
// (早期版本里整票级库位曾被反写过,避免老记录打开后明细全空)
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
? Entry?.WarehouseLocation
: locationFromArr;
var item = new RawMaterialSplitDetailItem
{
Portions = TryParseInt(GetAt(portionsArr, i)),
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
WarehouseLocation = locationFallback,
};
// 仅当后端已持久化了该行 ID 时才覆盖默认值,确保跨次保持稳定;
// 历史记录无 ID 字段时使用构造期生成的新 GUID之后保存会回填
var existingId = GetAt(idsArr, i);
if (!string.IsNullOrWhiteSpace(existingId))
{
item.Id = existingId;
}
// HasCard 行级解析(优先级 1从 PortionCardFlags 按位读取,"1"=已生成。
// 这是「保存后新增未生成行不被误判为已打印」的关键 — 新增行保存时 HasCard=false 持久化为 "0"
// 重新加载时回填 false「生成原材料卡片」就能正确把它列入待生成清单。
var flagAt = GetAt(cardFlagsArr, i);
if (!string.IsNullOrWhiteSpace(flagAt))
{
item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal);
}
else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId))
{
// 优先级 2兼容历史记录未存 PortionCardFlags 时,沿用旧逻辑——
// 持久化 ID 存在 + 整票已打印 ⇒ 视为已生成卡片。
item.HasCard = true;
}
// 否则保持构造默认值 false新增态、未打印整票等场景
SplitCodeDetails.Add(item);
}
RaisePropertyChanged(nameof(SplitCodeTableHeight));
}
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()
{
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<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 is nameof(RawMaterialSplitDetailItem.Portions)
or nameof(RawMaterialSplitDetailItem.PortionWeight)
or nameof(RawMaterialSplitDetailItem.PortionPackages))
{
RaiseSplitDisplayPropertyChanged();
}
}
/// <summary>
/// 拆码明细某行的「份数」变化时,按公式重算该行「每份重量」:
/// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数
/// — 用户单独手改「每份重量」不触发;
/// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过;
/// — 结果四舍五入到两位小数;
/// — 写入 row.PortionWeight 会触发 PropertyChanged(PortionWeight)不会再次进入本方法PropertyName 已不是 Portions不会形成循环。
/// </summary>
private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row)
{
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));
}
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.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);
}
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 => 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);
}
/// <summary>
/// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。
/// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断;
/// 「生成原材料卡片」成功后由 VM 置为 true「重新拆码」清空集合时随之失效。
/// true → 行内输入项 / 删除 全部锁定,避免与已生成卡片数据脱节;
/// false → 该行参与下次「生成原材料卡片」(继续拆码 + 续号生成)。
/// </summary>
private bool _hasCard;
public bool HasCard
{
get => _hasCard;
set => SetProperty(ref _hasCard, value);
}
}