新增密炼物料皮重策略功能,包括相关实体、服务、控制器及接口,支持桌面端免密CRUD操作,优化打印记录与原料入场记录的衍生字段填充逻辑,提升用户体验。
This commit is contained in:
@@ -9,6 +9,7 @@ using YY.Admin.Core;
|
||||
using YY.Admin.Core.Entity;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Services.Service;
|
||||
using YY.Admin.Services.Service.MixerMaterialTareStrategy;
|
||||
using YY.Admin.Views.RawMaterialEntry;
|
||||
using YY.Admin.ViewModels.WeightRecord;
|
||||
using YY.Admin.Views.WeightRecord;
|
||||
@@ -22,8 +23,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
protected IRawMaterialEntryService EntryService => _entryService;
|
||||
private readonly IJeecgDictSyncService _dictSyncService;
|
||||
private readonly IMixerMaterialService _mixerMaterialService;
|
||||
|
||||
// 加载完物料后用于回填 Edit 模式选中项
|
||||
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
|
||||
private readonly ISupplierService _supplierService;
|
||||
private bool _suppressTareStrategyRefresh;
|
||||
protected string? _pendingMaterialId;
|
||||
|
||||
private MesXslRawMaterialEntry? _entry;
|
||||
@@ -49,6 +51,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
|
||||
RaisePropertyChanged(nameof(HasSelectedMaterial));
|
||||
|
||||
_ = RefreshAllRowsTareStrategyAsync();
|
||||
|
||||
// 新增模式自动生成条码/批次号
|
||||
if (IsAddMode && !string.IsNullOrEmpty(value.MaterialCode))
|
||||
_ = AutoGenerateBarcodeAsync(value.MaterialCode);
|
||||
@@ -106,6 +110,27 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? EntryTimeInput
|
||||
{
|
||||
get => Entry?.EntryTime;
|
||||
set
|
||||
{
|
||||
if (Entry == null || Entry.EntryTime == value) return;
|
||||
Entry.EntryTime = value;
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
_ = RefreshAllRowsTareStrategyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>基础资料「托盘及皮重(合计)」= Σ 份数 × (包装物皮重 + 托盘重量)。</summary>
|
||||
public double PalletTareTotalDisplay => SplitCodeDetails.Sum(row =>
|
||||
{
|
||||
var portions = row.Portions ?? 0;
|
||||
var packaging = row.PackagingTare ?? 0d;
|
||||
var pallet = row.PalletWeight ?? 0d;
|
||||
return portions * (packaging + pallet);
|
||||
});
|
||||
|
||||
public ObservableCollection<MesMixerMaterial> MaterialOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
|
||||
public ObservableCollection<KeyValuePair<string, string>> TestStatusOptions { get; } = new();
|
||||
@@ -174,21 +199,27 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
public DelegateCommand ClearWeightRecordCommand { get; }
|
||||
public DelegateCommand OpenSupplierPickerCommand { get; }
|
||||
public DelegateCommand ClearSupplierCommand { get; }
|
||||
/// <summary>
|
||||
/// 拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。
|
||||
/// </summary>
|
||||
/// <summary>拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。</summary>
|
||||
public DelegateCommand<RawMaterialSplitDetailItem> OpenWarehouseAreaPickerCommand { get; }
|
||||
/// <summary>拆码明细 - 手动选择皮重策略。</summary>
|
||||
public DelegateCommand<RawMaterialSplitDetailItem> OpenTareStrategyPickerCommand { get; }
|
||||
/// <summary>拆码明细 - 清除手动策略并按规则重新自动匹配。</summary>
|
||||
public DelegateCommand<RawMaterialSplitDetailItem> ResetTareStrategyCommand { get; }
|
||||
|
||||
public RawMaterialEntryEditDialogViewModel(
|
||||
IRawMaterialEntryService entryService,
|
||||
IJeecgDictSyncService dictSyncService,
|
||||
IMixerMaterialService mixerMaterialService,
|
||||
IMixerMaterialTareStrategyService tareStrategyService,
|
||||
ISupplierService supplierService,
|
||||
IContainerExtension container,
|
||||
IRegionManager regionManager) : base(container, regionManager)
|
||||
{
|
||||
_entryService = entryService;
|
||||
_dictSyncService = dictSyncService;
|
||||
_mixerMaterialService = mixerMaterialService;
|
||||
_tareStrategyService = tareStrategyService;
|
||||
_supplierService = supplierService;
|
||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||
ResetCommand = new DelegateCommand(InitializeForAdd);
|
||||
@@ -201,6 +232,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync());
|
||||
ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection);
|
||||
OpenWarehouseAreaPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenWarehouseAreaPickerAsync(row));
|
||||
OpenTareStrategyPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenTareStrategyPickerAsync(row));
|
||||
ResetTareStrategyCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await ResetTareStrategyAsync(row));
|
||||
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged;
|
||||
_ = LoadAllAsync();
|
||||
}
|
||||
@@ -349,6 +382,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
RaisePropertyChanged(nameof(TotalWeightInput));
|
||||
RaisePropertyChanged(nameof(EntryTimeInput));
|
||||
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
|
||||
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
||||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||||
@@ -367,8 +402,13 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
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,
|
||||
ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, PalletTareTotal = entry.PalletTareTotal,
|
||||
TotalPortions = entry.TotalPortions,
|
||||
PortionWeight = entry.PortionWeight,
|
||||
PortionPackagingTare = entry.PortionPackagingTare,
|
||||
PortionPalletWeight = entry.PortionPalletWeight,
|
||||
PortionTareStrategyIds = entry.PortionTareStrategyIds,
|
||||
PortionPackages = entry.PortionPackages,
|
||||
PortionWarehouseLocations = entry.PortionWarehouseLocations,
|
||||
PortionDetailIds = entry.PortionDetailIds,
|
||||
PortionCardFlags = entry.PortionCardFlags,
|
||||
@@ -398,6 +438,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
RaisePropertyChanged(nameof(IsAddMode));
|
||||
RaisePropertyChanged(nameof(DialogTitle));
|
||||
RaisePropertyChanged(nameof(TotalWeightInput));
|
||||
RaisePropertyChanged(nameof(EntryTimeInput));
|
||||
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
|
||||
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
|
||||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||||
@@ -503,6 +545,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
Entry.BillNo = selected.BillNo;
|
||||
Entry.SupplierName = selected.SenderUnit;
|
||||
Entry.SupplierId = null;
|
||||
await ApplySupplierFromDisplayNameAsync(selected.SenderUnit);
|
||||
// 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」,
|
||||
// 用户仍可手动编辑;若净重为空则保留原值,避免误清空。
|
||||
if (selected.NetWeight.HasValue)
|
||||
@@ -514,6 +557,76 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
}
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
RaisePropertyChanged(nameof(TotalWeightInput));
|
||||
RaisePropertyChanged(nameof(EntryTimeInput));
|
||||
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
|
||||
await RefreshAllRowsTareStrategyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按供应商全称/简称反查 ID(榜单「发货单位」与手动填写的供应商名称均可用)。
|
||||
/// </summary>
|
||||
private async Task ApplySupplierFromDisplayNameAsync(string? displayName)
|
||||
{
|
||||
if (Entry == null || string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var supplier = await FindSupplierByDisplayNameAsync(displayName);
|
||||
if (supplier == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entry.SupplierId = supplier.Id;
|
||||
Entry.SupplierName = supplier.SupplierName
|
||||
?? supplier.SupplierShortName
|
||||
?? displayName.Trim();
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
}
|
||||
|
||||
private async Task<MesXslSupplier?> FindSupplierByDisplayNameAsync(string? displayName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = displayName.Trim();
|
||||
try
|
||||
{
|
||||
var page = await _supplierService.PageAsync(1, 5000);
|
||||
var exact = page.Records.FirstOrDefault(s =>
|
||||
string.Equals(s.SupplierName?.Trim(), name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(s.SupplierShortName?.Trim(), name, StringComparison.OrdinalIgnoreCase));
|
||||
if (exact != null)
|
||||
{
|
||||
return exact;
|
||||
}
|
||||
|
||||
return page.Records.FirstOrDefault(s =>
|
||||
(s.SupplierName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase)
|
||||
|| (s.SupplierShortName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains(s.SupplierShortName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains(s.SupplierName ?? "", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 皮重策略匹配前确保 SupplierId 已解析(榜单仅带出名称时补全 ID)。
|
||||
/// </summary>
|
||||
private async Task EnsureSupplierIdForTareMatchAsync()
|
||||
{
|
||||
if (Entry == null || !string.IsNullOrWhiteSpace(Entry.SupplierId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ApplySupplierFromDisplayNameAsync(Entry.SupplierName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -617,6 +730,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
Entry.SupplierId = null;
|
||||
Entry.SupplierName = null;
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
_ = RefreshAllRowsTareStrategyAsync();
|
||||
}
|
||||
|
||||
private async Task OpenSupplierPickerAsync()
|
||||
@@ -638,6 +752,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
Entry.SupplierId = selected.Id;
|
||||
Entry.SupplierName = selected.SupplierName;
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
await RefreshAllRowsTareStrategyAsync();
|
||||
}
|
||||
|
||||
private void ClearSupplierSelection()
|
||||
@@ -646,8 +761,128 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
Entry.SupplierId = null;
|
||||
Entry.SupplierName = null;
|
||||
RaisePropertyChanged(nameof(Entry));
|
||||
_ = RefreshAllRowsTareStrategyAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshAllRowsTareStrategyAsync()
|
||||
{
|
||||
if (_suppressTareStrategyRefresh) return;
|
||||
await EnsureSupplierIdForTareMatchAsync();
|
||||
foreach (var row in SplitCodeDetails.ToList())
|
||||
{
|
||||
if (!row.IsManualTareStrategy)
|
||||
await ApplyAutoTareStrategyToRowAsync(row);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyAutoTareStrategyToRowAsync(RawMaterialSplitDetailItem row)
|
||||
{
|
||||
if (_suppressTareStrategyRefresh || row.IsManualTareStrategy || row.HasCard) return;
|
||||
|
||||
try
|
||||
{
|
||||
var strategies = await _tareStrategyService.GetAllForMatchAsync();
|
||||
var match = MixerMaterialTareStrategyMatcher.PickBestMatch(
|
||||
strategies,
|
||||
Entry?.MaterialId,
|
||||
Entry?.SupplierId,
|
||||
Entry?.EntryTime,
|
||||
row.PortionWeight);
|
||||
ApplyTareStrategyToRow(row, match, manual: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ApplyTareStrategyToRow(row, null, manual: false);
|
||||
}
|
||||
|
||||
RaisePalletTareTotalChanged();
|
||||
}
|
||||
|
||||
private void ApplyTareStrategyToRow(
|
||||
RawMaterialSplitDetailItem row,
|
||||
MesXslMixerMaterialTareStrategy? strategy,
|
||||
bool manual)
|
||||
{
|
||||
if (strategy == null)
|
||||
{
|
||||
row.PackagingTare = 0d;
|
||||
row.PalletWeight = 0d;
|
||||
row.TareStrategyId = null;
|
||||
row.TareStrategyDisplay = "未匹配(0)";
|
||||
}
|
||||
else
|
||||
{
|
||||
row.PackagingTare = strategy.TareWeight.HasValue ? (double)strategy.TareWeight.Value : 0d;
|
||||
row.PalletWeight = strategy.PalletWeight.HasValue ? (double)strategy.PalletWeight.Value : 0d;
|
||||
row.TareStrategyId = strategy.Id;
|
||||
row.TareStrategyDisplay = BuildTareStrategyDisplay(strategy);
|
||||
}
|
||||
|
||||
row.IsManualTareStrategy = manual && strategy != null;
|
||||
}
|
||||
|
||||
private static string BuildTareStrategyDisplay(MesXslMixerMaterialTareStrategy strategy)
|
||||
{
|
||||
var spec = string.IsNullOrWhiteSpace(strategy.MaterialSpec) ? "-" : strategy.MaterialSpec;
|
||||
var pkg = strategy.TareWeight?.ToString("0.##") ?? "0";
|
||||
var pallet = strategy.PalletWeight?.ToString("0.##") ?? "0";
|
||||
return $"规格:{spec} 包装:{pkg} 托盘:{pallet}";
|
||||
}
|
||||
|
||||
private async Task OpenTareStrategyPickerAsync(RawMaterialSplitDetailItem? row)
|
||||
{
|
||||
if (row == null || row.HasCard) return;
|
||||
if (string.IsNullOrWhiteSpace(Entry?.MaterialId))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("请先选择密炼物料!");
|
||||
return;
|
||||
}
|
||||
|
||||
await EnsureSupplierIdForTareMatchAsync();
|
||||
if (string.IsNullOrWhiteSpace(Entry?.SupplierId))
|
||||
{
|
||||
HandyControl.Controls.MessageBox.Warning("未能匹配到供应商档案,请手动选择供应商,或检查榜单发货单位是否与供应商简称/全称一致!");
|
||||
return;
|
||||
}
|
||||
|
||||
TareStrategyPickerDialogViewModel? pickerVm = null;
|
||||
bool confirmed;
|
||||
try
|
||||
{
|
||||
confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(async () =>
|
||||
{
|
||||
return await HandyControl.Controls.Dialog.Show<TareStrategyPickerDialogView>()
|
||||
.Initialize<TareStrategyPickerDialogViewModel>(vm =>
|
||||
{
|
||||
pickerVm = vm;
|
||||
vm.Initialize(Entry!.MaterialId, Entry.SupplierId, Entry.EntryTime, row.PortionWeight, row.TareStrategyId);
|
||||
})
|
||||
.GetResultAsync<bool>();
|
||||
});
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (!confirmed) return;
|
||||
if (pickerVm?.SelectedStrategy == null)
|
||||
{
|
||||
row.IsManualTareStrategy = false;
|
||||
await ApplyAutoTareStrategyToRowAsync(row);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTareStrategyToRow(row, pickerVm.SelectedStrategy, manual: true);
|
||||
RaisePalletTareTotalChanged();
|
||||
}
|
||||
|
||||
private async Task ResetTareStrategyAsync(RawMaterialSplitDetailItem? row)
|
||||
{
|
||||
if (row == null || row.HasCard) return;
|
||||
row.IsManualTareStrategy = false;
|
||||
await ApplyAutoTareStrategyToRowAsync(row);
|
||||
}
|
||||
|
||||
private void RaisePalletTareTotalChanged() => RaisePropertyChanged(nameof(PalletTareTotalDisplay));
|
||||
|
||||
private void RecalculateShelfLife(MesMixerMaterial? material)
|
||||
{
|
||||
if (Entry == null || material?.ShelfLifeDays == null || material.ShelfLifeDays <= 0)
|
||||
@@ -719,63 +954,68 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
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++)
|
||||
_suppressTareStrategyRefresh = true;
|
||||
try
|
||||
{
|
||||
var locationFromArr = GetAt(locationsArr, i);
|
||||
// 兼容历史数据:明细库位拼接字段为空时,首行回退到 Entry.WarehouseLocation
|
||||
// (早期版本里整票级库位曾被反写过,避免老记录打开后明细全空)
|
||||
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
|
||||
? Entry?.WarehouseLocation
|
||||
: locationFromArr;
|
||||
SplitCodeDetails.Clear();
|
||||
|
||||
var item = new RawMaterialSplitDetailItem
|
||||
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
|
||||
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
|
||||
var packagingTareArr = SplitJoinedValues(Entry?.PortionPackagingTare);
|
||||
var palletWeightArr = SplitJoinedValues(Entry?.PortionPalletWeight);
|
||||
var strategyIdsArr = SplitJoinedValues(Entry?.PortionTareStrategyIds);
|
||||
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
|
||||
var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations);
|
||||
var idsArr = SplitJoinedValues(Entry?.PortionDetailIds);
|
||||
var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags);
|
||||
var fallbackToPrintFlag = cardFlagsArr.Length == 0
|
||||
&& string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
|
||||
var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length),
|
||||
Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length)));
|
||||
|
||||
for (var i = 0; i < rowCount; i++)
|
||||
{
|
||||
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;
|
||||
var locationFromArr = GetAt(locationsArr, i);
|
||||
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
|
||||
? Entry?.WarehouseLocation
|
||||
: locationFromArr;
|
||||
|
||||
var strategyId = GetAt(strategyIdsArr, i);
|
||||
var item = new RawMaterialSplitDetailItem
|
||||
{
|
||||
Portions = TryParseInt(GetAt(portionsArr, i)),
|
||||
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
|
||||
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
|
||||
WarehouseLocation = locationFallback,
|
||||
PackagingTare = TryParseDouble(GetAt(packagingTareArr, i)) ?? 0d,
|
||||
PalletWeight = TryParseDouble(GetAt(palletWeightArr, i)) ?? 0d,
|
||||
TareStrategyId = strategyId,
|
||||
IsManualTareStrategy = !string.IsNullOrWhiteSpace(strategyId),
|
||||
};
|
||||
item.TareStrategyDisplay = string.IsNullOrWhiteSpace(strategyId)
|
||||
? "未匹配(0)"
|
||||
: $"策略ID:{strategyId}";
|
||||
|
||||
var existingId = GetAt(idsArr, i);
|
||||
if (!string.IsNullOrWhiteSpace(existingId))
|
||||
item.Id = existingId;
|
||||
|
||||
var flagAt = GetAt(cardFlagsArr, i);
|
||||
if (!string.IsNullOrWhiteSpace(flagAt))
|
||||
item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal);
|
||||
else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId))
|
||||
item.HasCard = true;
|
||||
|
||||
SplitCodeDetails.Add(item);
|
||||
}
|
||||
// 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));
|
||||
RaisePalletTareTotalChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressTareStrategyRefresh = false;
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
||||
}
|
||||
|
||||
private static string[] SplitJoinedValues(string? value)
|
||||
@@ -798,8 +1038,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
private void AddSplitDetailRow()
|
||||
{
|
||||
SplitCodeDetails.Add(new RawMaterialSplitDetailItem());
|
||||
var row = new RawMaterialSplitDetailItem();
|
||||
SplitCodeDetails.Add(row);
|
||||
RaisePropertyChanged(nameof(SplitCodeTableHeight));
|
||||
_ = ApplyAutoTareStrategyToRowAsync(row);
|
||||
}
|
||||
|
||||
private void RemoveSplitDetailRow(RawMaterialSplitDetailItem? item)
|
||||
@@ -841,32 +1083,52 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// 用户填写或修改某行的「份数」时,按公式自动算出该行「每份重量」:
|
||||
// 每份重量 = (总重 - Σ其他行的份数×每份重量) / 当前行份数
|
||||
// 用户修改某行的「份数」时:仅当该行「每份重量」为空才按公式自动计算
|
||||
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions)
|
||||
&& sender is RawMaterialSplitDetailItem row)
|
||||
{
|
||||
RecalculatePortionWeightForRow(row);
|
||||
}
|
||||
|
||||
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.PortionWeight)
|
||||
&& sender is RawMaterialSplitDetailItem weightRow)
|
||||
{
|
||||
// 清空每份重量后,若份数已填则立即按公式重算(配合 PortionWeightText 空值绑定)
|
||||
if (!weightRow.PortionWeight.HasValue)
|
||||
{
|
||||
RecalculatePortionWeightForRow(weightRow);
|
||||
}
|
||||
|
||||
if (!weightRow.IsManualTareStrategy && !weightRow.HasCard)
|
||||
{
|
||||
_ = ApplyAutoTareStrategyToRowAsync(weightRow);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions)
|
||||
or nameof(RawMaterialSplitDetailItem.PortionWeight)
|
||||
or nameof(RawMaterialSplitDetailItem.PortionPackages))
|
||||
or nameof(RawMaterialSplitDetailItem.PortionPackages)
|
||||
or nameof(RawMaterialSplitDetailItem.PackagingTare)
|
||||
or nameof(RawMaterialSplitDetailItem.PalletWeight))
|
||||
{
|
||||
RaiseSplitDisplayPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拆码明细某行的「份数」变化时,按公式重算该行「每份重量」:
|
||||
/// 拆码明细某行的「份数」变化时,仅当该行「每份重量」为空时按公式重算:
|
||||
/// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数
|
||||
/// — 用户单独手改「每份重量」不触发;
|
||||
/// — 已填写「每份重量」后再改份数,不覆盖用户输入;
|
||||
/// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过;
|
||||
/// — 结果四舍五入到两位小数;
|
||||
/// — 写入 row.PortionWeight 会触发 PropertyChanged(PortionWeight),不会再次进入本方法(PropertyName 已不是 Portions),不会形成循环。
|
||||
/// — 结果四舍五入到两位小数。
|
||||
/// </summary>
|
||||
private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row)
|
||||
{
|
||||
if (row.PortionWeight.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Entry?.TotalWeight is not { } total || total <= 0d)
|
||||
{
|
||||
return;
|
||||
@@ -898,6 +1160,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
|
||||
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
|
||||
RaisePalletTareTotalChanged();
|
||||
}
|
||||
|
||||
private string JoinSplitValue(Func<RawMaterialSplitDetailItem, string?> selector, bool withTrailingSlash)
|
||||
@@ -938,6 +1201,11 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
|
||||
Entry.TotalPortions = JoinSplitValue(it => it.Portions?.ToString(CultureInfo.InvariantCulture), true);
|
||||
Entry.PortionWeight = JoinSplitValue(it => FormatNullableDecimal(it.PortionWeight), true);
|
||||
Entry.PortionPackagingTare = JoinSplitValue(it => FormatNullableDecimal(it.PackagingTare), true);
|
||||
Entry.PortionPalletWeight = JoinSplitValue(it => FormatNullableDecimal(it.PalletWeight), true);
|
||||
Entry.PortionTareStrategyIds = JoinSplitValue(
|
||||
it => it.IsManualTareStrategy && !string.IsNullOrWhiteSpace(it.TareStrategyId) ? it.TareStrategyId : null,
|
||||
true);
|
||||
Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true);
|
||||
Entry.PortionWarehouseLocations = JoinSplitValue(it => string.IsNullOrWhiteSpace(it.WarehouseLocation) ? null : it.WarehouseLocation.Trim(), true);
|
||||
// 持久化每行 GUID,行序与上方四个字段对齐;JoinSplitValue 会过滤空,但 Id 在构造时即生成不会为空。
|
||||
@@ -947,6 +1215,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
||||
// 「生成原材料卡片」只对 HasCard=false 的行加卡 → 必须把真实的 HasCard 持久化,
|
||||
// 否则重新加载后无法区分「已生成」与「保存后新增的未生成行」。
|
||||
Entry.PortionCardFlags = JoinSplitValue(it => it.HasCard ? "1" : "0", true);
|
||||
Entry.PalletTareTotal = PalletTareTotalDisplay;
|
||||
}
|
||||
|
||||
private double CalculateSplitCodeTableHeight()
|
||||
@@ -983,7 +1252,37 @@ public class RawMaterialSplitDetailItem : BindableBase
|
||||
public double? PortionWeight
|
||||
{
|
||||
get => _portionWeight;
|
||||
set => SetProperty(ref _portionWeight, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _portionWeight, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(PortionWeightText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每份重量文本(供 TextBox 绑定;空字符串表示未填写,避免 double? 直接绑定无法清空)。
|
||||
/// </summary>
|
||||
public string PortionWeightText
|
||||
{
|
||||
get => _portionWeight.HasValue
|
||||
? _portionWeight.Value.ToString("0.##", CultureInfo.InvariantCulture)
|
||||
: string.Empty;
|
||||
set
|
||||
{
|
||||
var text = value?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
PortionWeight = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
PortionWeight = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int? _portionPackages;
|
||||
@@ -1000,6 +1299,42 @@ public class RawMaterialSplitDetailItem : BindableBase
|
||||
set => SetProperty(ref _warehouseLocation, value);
|
||||
}
|
||||
|
||||
private double? _packagingTare;
|
||||
public double? PackagingTare
|
||||
{
|
||||
get => _packagingTare;
|
||||
set => SetProperty(ref _packagingTare, value);
|
||||
}
|
||||
|
||||
private double? _palletWeight;
|
||||
public double? PalletWeight
|
||||
{
|
||||
get => _palletWeight;
|
||||
set => SetProperty(ref _palletWeight, value);
|
||||
}
|
||||
|
||||
private string? _tareStrategyId;
|
||||
public string? TareStrategyId
|
||||
{
|
||||
get => _tareStrategyId;
|
||||
set => SetProperty(ref _tareStrategyId, value);
|
||||
}
|
||||
|
||||
private string? _tareStrategyDisplay = "未匹配(0)";
|
||||
public string? TareStrategyDisplay
|
||||
{
|
||||
get => _tareStrategyDisplay;
|
||||
set => SetProperty(ref _tareStrategyDisplay, value);
|
||||
}
|
||||
|
||||
private bool _isManualTareStrategy;
|
||||
/// <summary>用户手动选择皮重策略后为 true,自动匹配不再覆盖。</summary>
|
||||
public bool IsManualTareStrategy
|
||||
{
|
||||
get => _isManualTareStrategy;
|
||||
set => SetProperty(ref _isManualTareStrategy, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。
|
||||
/// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断;
|
||||
|
||||
Reference in New Issue
Block a user