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

966 lines
39 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.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();
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)
{
HandyControl.Controls.MessageBox.Success("新增成功!");
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 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 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 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 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);
}
}