新增密炼物料皮重策略功能,包括相关实体、服务、控制器及接口,支持桌面端免密CRUD操作,优化打印记录与原料入场记录的衍生字段填充逻辑,提升用户体验。

This commit is contained in:
geht
2026-06-02 16:28:51 +08:00
parent 37239e1b0a
commit fef7d25e3c
75 changed files with 4407 additions and 170 deletions

View File

@@ -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 推断;