2026-05-09 15:55:11 +08:00
|
|
|
|
using HandyControl.Controls;
|
2026-05-14 15:55:54 +08:00
|
|
|
|
using HandyControl.Data;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
using HandyControl.Tools.Extension;
|
2026-05-09 19:10:42 +08:00
|
|
|
|
using System.Collections.Specialized;
|
|
|
|
|
|
using System.ComponentModel;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
using System.Collections.ObjectModel;
|
2026-05-09 19:10:42 +08:00
|
|
|
|
using System.Globalization;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
using YY.Admin.Core;
|
|
|
|
|
|
using YY.Admin.Core.Entity;
|
|
|
|
|
|
using YY.Admin.Core.Services;
|
|
|
|
|
|
using YY.Admin.Services.Service;
|
2026-05-09 18:25:34 +08:00
|
|
|
|
using YY.Admin.Views.RawMaterialEntry;
|
2026-05-11 14:32:44 +08:00
|
|
|
|
using YY.Admin.ViewModels.WeightRecord;
|
|
|
|
|
|
using YY.Admin.Views.WeightRecord;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
|
|
|
|
|
|
|
|
|
|
|
public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResultable<bool>
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly IRawMaterialEntryService _entryService;
|
2026-05-11 14:32:44 +08:00
|
|
|
|
/// <summary>供子页面(如独立新增页右侧面板)复用列表/详情查询能力。</summary>
|
|
|
|
|
|
protected IRawMaterialEntryService EntryService => _entryService;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
private readonly IJeecgDictSyncService _dictSyncService;
|
|
|
|
|
|
private readonly IMixerMaterialService _mixerMaterialService;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载完物料后用于回填 Edit 模式选中项
|
2026-05-11 14:32:44 +08:00
|
|
|
|
protected string? _pendingMaterialId;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
private MesXslRawMaterialEntry? _entry;
|
|
|
|
|
|
public MesXslRawMaterialEntry? Entry
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _entry;
|
|
|
|
|
|
set => SetProperty(ref _entry, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
protected MesMixerMaterial? _selectedMaterial;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
public MesMixerMaterial? SelectedMaterial
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _selectedMaterial;
|
|
|
|
|
|
set
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!SetProperty(ref _selectedMaterial, value) || value == null || Entry == null) return;
|
2026-05-09 18:25:34 +08:00
|
|
|
|
Entry.MaterialId = value.Id;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
Entry.MaterialCode = value.MaterialCode;
|
|
|
|
|
|
Entry.MaterialName = value.MaterialName;
|
2026-05-09 18:25:34 +08:00
|
|
|
|
Entry.ManufacturerMaterialName = value.AliasName;
|
|
|
|
|
|
RecalculateShelfLife(value);
|
2026-05-09 15:55:11 +08:00
|
|
|
|
RaisePropertyChanged(nameof(Entry));
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 新增模式自动生成条码/批次号
|
|
|
|
|
|
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 ? "新增原料入场记录" : "编辑原料入场记录";
|
2026-05-12 14:06:07 +08:00
|
|
|
|
/// <summary>已打印(PrintFlag=="1"):总重只读、不可新增明细。</summary>
|
|
|
|
|
|
public bool IsPrinted => string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
|
|
|
|
|
|
public bool IsTotalWeightEditable => !IsPrinted;
|
2026-05-09 18:25:34 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
2026-05-09 18:25:34 +08:00
|
|
|
|
public ObservableCollection<RawMaterialSplitDetailItem> SplitCodeDetails { get; } = new();
|
|
|
|
|
|
public double SplitCodeTableHeight => CalculateSplitCodeTableHeight();
|
2026-05-09 19:10:42 +08:00
|
|
|
|
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);
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
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; }
|
2026-05-09 18:25:34 +08:00
|
|
|
|
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; }
|
2026-05-11 14:32:44 +08:00
|
|
|
|
public DelegateCommand OpenSupplierPickerCommand { get; }
|
|
|
|
|
|
public DelegateCommand ClearSupplierCommand { get; }
|
2026-05-12 14:06:07 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public DelegateCommand<RawMaterialSplitDetailItem> OpenWarehouseAreaPickerCommand { get; }
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
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());
|
2026-05-09 18:25:34 +08:00
|
|
|
|
ResetCommand = new DelegateCommand(InitializeForAdd);
|
2026-05-12 14:06:07 +08:00
|
|
|
|
AddSplitDetailCommand = new DelegateCommand(AddSplitDetailRow, () => !IsPrinted);
|
2026-05-09 18:25:34 +08:00
|
|
|
|
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);
|
2026-05-11 14:32:44 +08:00
|
|
|
|
OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync());
|
|
|
|
|
|
ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection);
|
2026-05-12 14:06:07 +08:00
|
|
|
|
OpenWarehouseAreaPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenWarehouseAreaPickerAsync(row));
|
2026-05-09 19:10:42 +08:00
|
|
|
|
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged;
|
2026-05-09 15:55:11 +08:00
|
|
|
|
_ = LoadAllAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task LoadAllAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.WhenAll(LoadDictOptionsAsync(), LoadMaterialOptionsAsync());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
protected async Task LoadMaterialOptionsAsync()
|
2026-05-09 15:55:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
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>>());
|
2026-05-09 18:25:34 +08:00
|
|
|
|
ApplyDefaultEntryStatusForAdd();
|
2026-05-11 14:32:44 +08:00
|
|
|
|
ApplyHiddenFieldDefaultsForAdd();
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
FillFallbackOptions();
|
|
|
|
|
|
ApplyDefaultEntryStatusForAdd();
|
2026-05-11 14:32:44 +08:00
|
|
|
|
ApplyHiddenFieldDefaultsForAdd();
|
2026-05-09 15:55:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
protected async Task AutoGenerateBarcodeAsync(string materialCode)
|
2026-05-09 15:55:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
public virtual void InitializeForAdd()
|
2026-05-09 15:55:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
_selectedMaterial = null;
|
|
|
|
|
|
RaisePropertyChanged(nameof(SelectedMaterial));
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
2026-05-09 15:55:11 +08:00
|
|
|
|
Entry = new MesXslRawMaterialEntry
|
|
|
|
|
|
{
|
2026-05-09 18:25:34 +08:00
|
|
|
|
EntryTime = DateTime.Now,
|
|
|
|
|
|
IsSpecialAdoption = "0"
|
2026-05-09 15:55:11 +08:00
|
|
|
|
};
|
2026-05-09 18:25:34 +08:00
|
|
|
|
InitializeSplitCodeDetailsFromEntry();
|
|
|
|
|
|
ApplyDefaultEntryStatusForAdd();
|
2026-05-11 14:32:44 +08:00
|
|
|
|
ApplyHiddenFieldDefaultsForAdd();
|
2026-05-09 15:55:11 +08:00
|
|
|
|
RaisePropertyChanged(nameof(IsAddMode));
|
|
|
|
|
|
RaisePropertyChanged(nameof(DialogTitle));
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(TotalWeightInput));
|
|
|
|
|
|
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
2026-05-09 19:10:42 +08:00
|
|
|
|
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
2026-05-12 14:06:07 +08:00
|
|
|
|
RaisePropertyChanged(nameof(IsPrinted));
|
|
|
|
|
|
RaisePropertyChanged(nameof(IsTotalWeightEditable));
|
|
|
|
|
|
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
2026-05-09 15:55:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2026-05-12 14:06:07 +08:00
|
|
|
|
PortionWarehouseLocations = entry.PortionWarehouseLocations,
|
|
|
|
|
|
PortionDetailIds = entry.PortionDetailIds,
|
|
|
|
|
|
PortionCardFlags = entry.PortionCardFlags,
|
2026-05-09 15:55:11 +08:00
|
|
|
|
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,
|
|
|
|
|
|
};
|
2026-05-09 18:25:34 +08:00
|
|
|
|
InitializeSplitCodeDetailsFromEntry();
|
2026-05-09 15:55:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 若物料列表已加载则直接回填,否则记录 pending 等加载完后回填
|
|
|
|
|
|
if (MaterialOptions.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
_selectedMaterial = MaterialOptions.FirstOrDefault(m =>
|
|
|
|
|
|
string.Equals(m.Id, entry.MaterialId, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
RaisePropertyChanged(nameof(SelectedMaterial));
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
2026-05-09 15:55:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_pendingMaterialId = entry.MaterialId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RaisePropertyChanged(nameof(IsAddMode));
|
|
|
|
|
|
RaisePropertyChanged(nameof(DialogTitle));
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(TotalWeightInput));
|
|
|
|
|
|
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
2026-05-09 19:10:42 +08:00
|
|
|
|
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
|
|
|
|
|
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
2026-05-12 14:06:07 +08:00
|
|
|
|
RaisePropertyChanged(nameof(IsPrinted));
|
|
|
|
|
|
RaisePropertyChanged(nameof(IsTotalWeightEditable));
|
|
|
|
|
|
AddSplitDetailCommand.RaiseCanExecuteChanged();
|
2026-05-09 15:55:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 10:43:51 +08:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
protected virtual async Task SaveAsync()
|
2026-05-09 15:55:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (Entry == null) return;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-14 10:43:51 +08:00
|
|
|
|
if (!await PersistEntryCoreAsync()) return;
|
2026-05-12 14:06:07 +08:00
|
|
|
|
|
2026-05-09 15:55:11 +08:00
|
|
|
|
if (IsAddMode)
|
|
|
|
|
|
{
|
2026-05-14 15:55:54 +08:00
|
|
|
|
// 非模态轻提示,约 1 秒后自动消失(与独立页「保存并打印」时的体验一致)
|
|
|
|
|
|
Growl.Success(new GrowlInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
Message = "保存成功",
|
|
|
|
|
|
ShowDateTime = false,
|
|
|
|
|
|
WaitTime = 1
|
|
|
|
|
|
});
|
2026-05-14 10:43:51 +08:00
|
|
|
|
if (CloseAction == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 独立新增页面:保存成功后自动清空表单,便于连续录入
|
|
|
|
|
|
InitializeForAdd();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 15:55:11 +08:00
|
|
|
|
CloseAction?.Invoke();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-09 18:25:34 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-12 14:06:07 +08:00
|
|
|
|
// 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」,
|
|
|
|
|
|
// 用户仍可手动编辑;若净重为空则保留原值,避免误清空。
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(Entry));
|
2026-05-12 14:06:07 +08:00
|
|
|
|
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;
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
private async Task OpenSupplierPickerAsync()
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
2026-05-11 14:32:44 +08:00
|
|
|
|
SupplierPickerDialogViewModel? pickerVm = null;
|
|
|
|
|
|
bool confirmed;
|
|
|
|
|
|
try
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
2026-05-11 14:32:44 +08:00
|
|
|
|
confirmed = await HandyControl.Controls.Dialog.Show<SupplierPickerDialogView>()
|
|
|
|
|
|
.Initialize<SupplierPickerDialogViewModel>(vm => pickerVm = vm)
|
|
|
|
|
|
.GetResultAsync<bool>();
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
2026-05-11 14:32:44 +08:00
|
|
|
|
catch { return; }
|
2026-05-09 18:25:34 +08:00
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
if (!confirmed || pickerVm?.SelectedSupplier == null || Entry == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
var selected = pickerVm.SelectedSupplier;
|
|
|
|
|
|
Entry.SupplierId = selected.Id;
|
|
|
|
|
|
Entry.SupplierName = selected.SupplierName;
|
|
|
|
|
|
RaisePropertyChanged(nameof(Entry));
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
private void ClearSupplierSelection()
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
2026-05-11 14:32:44 +08:00
|
|
|
|
if (Entry == null) return;
|
|
|
|
|
|
Entry.SupplierId = null;
|
|
|
|
|
|
Entry.SupplierName = null;
|
|
|
|
|
|
RaisePropertyChanged(nameof(Entry));
|
|
|
|
|
|
}
|
2026-05-09 18:25:34 +08:00
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
private void RecalculateShelfLife(MesMixerMaterial? material)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Entry == null || material?.ShelfLifeDays == null || material.ShelfLifeDays <= 0)
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
Entry.ShelfLife = DateTime.Now.Date.AddDays(material.ShelfLifeDays.Value).ToString("yyyy-MM-dd");
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
protected void ApplyDefaultEntryStatusForAdd()
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
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";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 新增模式下为已在前端隐藏的字段补充默认值,确保保存到后端时不为空:
|
2026-05-12 14:06:07 +08:00
|
|
|
|
/// 检测结果=未检、打印标记=未打印、入库结存=否。
|
2026-05-11 14:32:44 +08:00
|
|
|
|
/// 字典就绪时取字典 code,未就绪时回退到约定的常用 code。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected void ApplyHiddenFieldDefaultsForAdd()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!IsAddMode || Entry == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(Entry.TestResult))
|
|
|
|
|
|
{
|
|
|
|
|
|
Entry.TestResult = ResolveDefaultOptionValue(TestResultOptions, "未检", "0");
|
|
|
|
|
|
}
|
2026-05-12 14:06:07 +08:00
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
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()
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
SplitCodeDetails.Clear();
|
2026-05-11 14:32:44 +08:00
|
|
|
|
|
2026-05-12 14:06:07 +08:00
|
|
|
|
// 六个字段都是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
|
2026-05-11 14:32:44 +08:00
|
|
|
|
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
|
|
|
|
|
|
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
|
|
|
|
|
|
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
|
2026-05-12 14:06:07 +08:00
|
|
|
|
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)));
|
2026-05-11 14:32:44 +08:00
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < rowCount; i++)
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
2026-05-12 14:06:07 +08:00
|
|
|
|
var locationFromArr = GetAt(locationsArr, i);
|
|
|
|
|
|
// 兼容历史数据:明细库位拼接字段为空时,首行回退到 Entry.WarehouseLocation
|
|
|
|
|
|
// (早期版本里整票级库位曾被反写过,避免老记录打开后明细全空)
|
|
|
|
|
|
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
|
|
|
|
|
|
? Entry?.WarehouseLocation
|
|
|
|
|
|
: locationFromArr;
|
|
|
|
|
|
|
|
|
|
|
|
var item = new RawMaterialSplitDetailItem
|
2026-05-11 14:32:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
Portions = TryParseInt(GetAt(portionsArr, i)),
|
|
|
|
|
|
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
|
|
|
|
|
|
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
|
2026-05-12 14:06:07 +08:00
|
|
|
|
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);
|
2026-05-11 14:32:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 18:25:34 +08:00
|
|
|
|
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-05-09 18:25:34 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 19:10:42 +08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-05-12 14:06:07 +08:00
|
|
|
|
// 用户填写或修改某行的「份数」时,按公式自动算出该行「每份重量」:
|
|
|
|
|
|
// 每份重量 = (总重 - Σ其他行的份数×每份重量) / 当前行份数
|
|
|
|
|
|
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions)
|
|
|
|
|
|
&& sender is RawMaterialSplitDetailItem row)
|
|
|
|
|
|
{
|
|
|
|
|
|
RecalculatePortionWeightForRow(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 19:10:42 +08:00
|
|
|
|
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions)
|
|
|
|
|
|
or nameof(RawMaterialSplitDetailItem.PortionWeight)
|
|
|
|
|
|
or nameof(RawMaterialSplitDetailItem.PortionPackages))
|
|
|
|
|
|
{
|
|
|
|
|
|
RaiseSplitDisplayPropertyChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 14:06:07 +08:00
|
|
|
|
/// <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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 19:10:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 14:32:44 +08:00
|
|
|
|
/// <summary>
|
2026-05-12 14:06:07 +08:00
|
|
|
|
/// 把 SplitCodeDetails 全部明细行的「份数 / 每份重量 / 每份包数 / 库位 / 行 ID」按 "x/y/z/" 拼接后持久化到 Entry。
|
|
|
|
|
|
/// 明细行的「库位」属于行级数据(用于后续生成原材料卡片时区分库位),与 Entry.WarehouseLocation
|
|
|
|
|
|
/// (基础资料整票级单值)独立保存,通过 portion_warehouse_locations 字段持久化。
|
|
|
|
|
|
/// 子类(如 OperationViewModel 的「重新拆码」)需要直接调用,因此声明为 protected。
|
2026-05-11 14:32:44 +08:00
|
|
|
|
/// </summary>
|
2026-05-12 14:06:07 +08:00
|
|
|
|
protected void ApplySplitDetailsToEntry()
|
2026-05-09 18:25:34 +08:00
|
|
|
|
{
|
2026-05-11 14:32:44 +08:00
|
|
|
|
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);
|
2026-05-12 14:06:07 +08:00
|
|
|
|
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);
|
2026-05-09 18:25:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private double CalculateSplitCodeTableHeight()
|
|
|
|
|
|
{
|
2026-05-11 14:32:44 +08:00
|
|
|
|
// 与拆码明细 XAML 行高保持一致:表头 40px、数据行 44px
|
|
|
|
|
|
const double headerHeight = 40d;
|
|
|
|
|
|
const double rowHeight = 44d;
|
2026-05-09 18:25:34 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-05-12 14:06:07 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 拆码明细行 ID(GUID,构造时生成)。前端隐藏;
|
|
|
|
|
|
/// 「生成原材料卡片」时由桌面端写入对应 MesXslRawMaterialCard.SplitDetailId;
|
|
|
|
|
|
/// 「重新拆码」时按入场记录的 PortionDetailIds 反查批删关联卡片。
|
|
|
|
|
|
/// 编辑回填时若 portion_detail_ids 已有该位置的值,会覆盖默认值,确保跨次保持稳定。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
|
|
|
|
|
|
2026-05-09 18:25:34 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-05-12 14:06:07 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。
|
|
|
|
|
|
/// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断;
|
|
|
|
|
|
/// 「生成原材料卡片」成功后由 VM 置为 true;「重新拆码」清空集合时随之失效。
|
|
|
|
|
|
/// true → 行内输入项 / 删除 全部锁定,避免与已生成卡片数据脱节;
|
|
|
|
|
|
/// false → 该行参与下次「生成原材料卡片」(继续拆码 + 续号生成)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private bool _hasCard;
|
|
|
|
|
|
public bool HasCard
|
|
|
|
|
|
{
|
|
|
|
|
|
get => _hasCard;
|
|
|
|
|
|
set => SetProperty(ref _hasCard, value);
|
|
|
|
|
|
}
|
2026-05-09 15:55:11 +08:00
|
|
|
|
}
|