桌面端原材料入场记录完善

This commit is contained in:
2026-07-31 14:27:46 +08:00
parent 67f3acfd43
commit ef28b93909
13 changed files with 531 additions and 75 deletions

View File

@@ -57,11 +57,18 @@ public class PrintDotService : IPrintDotService
public async Task PrintAsync(string printerName, string pdfBase64, string jobName = "QH-MES", int copies = 1, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(printerName))
throw new InvalidOperationException("打印机名称不能为空");
if (string.IsNullOrWhiteSpace(pdfBase64))
throw new InvalidOperationException("打印内容为空PDF 生成失败");
// 去掉 data: 前缀
var content = pdfBase64.Trim();
var comma = content.IndexOf(',');
if (content.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && comma >= 0)
content = content[(comma + 1)..];
if (string.IsNullOrWhiteSpace(content))
throw new InvalidOperationException("打印内容为空PDF 生成失败");
var payload = new
{
@@ -87,14 +94,46 @@ public class PrintDotService : IPrintDotService
{
var response = await ReceiveTextAsync(ws, cts.Token);
var resDoc = JsonNode.Parse(response);
var status = resDoc?["status"]?.GetValue<string>();
if (status == null) continue;
if (status == "success") return;
var rawMsg = resDoc?["message"]?.GetValue<string>() ?? "PrintDot 打印失败";
var status = TryReadJsonString(resDoc?["status"]);
if (string.IsNullOrWhiteSpace(status)) continue;
if (string.Equals(status, "success", StringComparison.OrdinalIgnoreCase)) return;
var rawMsg = TryReadJsonString(resDoc?["message"]) ?? "PrintDot 打印失败";
//update-begin---author:jiangxh ---date:20260731 for【PrintDot】队列未入队超时按软成功虚拟打印机-----------
if (IsPrintQueueAppearSoftSuccess(rawMsg))
{
return;
}
//update-end---author:jiangxh ---date:20260731 for【PrintDot】队列未入队超时按软成功虚拟打印机-----------
throw new InvalidOperationException(EnhanceErrorMessage(rawMsg));
}
}
private static string? TryReadJsonString(JsonNode? node)
{
if (node == null) return null;
try
{
if (node is JsonValue jv)
{
if (jv.TryGetValue<string>(out var s)) return s;
if (jv.TryGetValue<bool>(out var b)) return b ? "true" : "false";
return jv.ToJsonString().Trim('"');
}
return node.ToJsonString();
}
catch
{
return null;
}
}
/// <summary>PrintDot 队列入队超时:视为已提交(兼容旧版桥接器)。</summary>
private static bool IsPrintQueueAppearSoftSuccess(string? rawMsg)
{
if (string.IsNullOrWhiteSpace(rawMsg)) return false;
return rawMsg.Contains("print job not queued within", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 将 PrintDot 返回的部分英文错误转换为带本地处理步骤的中文提示。
/// 与 web 端 printDotBridge.ts::enhancePrintDotErrorMessage 行为一致,方便桌面端用户自助排查。

View File

@@ -159,7 +159,14 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
try
{
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
if (ok) { UpsertLocalCache(local); return true; }
if (ok)
{
//update-begin---author:jiangxh ---date:20260731 for【原料入场】新增后回写Id/条码,避免保存并打印空引用-----------
CopyGeneratedFieldsToEntry(entry, local);
UpsertLocalCache(local);
return true;
//update-end---author:jiangxh ---date:20260731 for【原料入场】新增后回写Id/条码,避免保存并打印空引用-----------
}
return false;
}
catch (Exception ex)
@@ -169,8 +176,21 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
}
EnqueuePendingOperation(new EntryPendingOperation { OpType = EntryOpType.Add, EntryId = local.Id, Entry = local, CreatedAt = DateTime.UtcNow });
//update-begin---author:jiangxh ---date:20260731 for【原料入场】新增后回写Id/条码,避免保存并打印空引用-----------
CopyGeneratedFieldsToEntry(entry, local);
UpsertLocalCache(local);
return true;
//update-end---author:jiangxh ---date:20260731 for【原料入场】新增后回写Id/条码,避免保存并打印空引用-----------
}
/// <summary>把 Clone 上生成的主键/条码/批次写回调用方 Entry供保存并打印继续使用。</summary>
private static void CopyGeneratedFieldsToEntry(MesXslRawMaterialEntry entry, MesXslRawMaterialEntry local)
{
if (entry == null || local == null) return;
entry.Id = local.Id;
entry.Barcode = local.Barcode;
entry.BatchNo = local.BatchNo;
entry.TenantId = local.TenantId;
}
public async Task<bool> EditAsync(MesXslRawMaterialEntry entry, CancellationToken ct = default)
@@ -586,7 +606,51 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
var url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/add?tenantId={DefaultTenantId}";
var payload = Clone(entry);
if (IsLocalTempId(payload.Id)) payload.Id = null;
return await PostJsonAsync(url, payload, ct).ConfigureAwait(false);
//update-begin---author:jiangxh ---date:20260731 for【原料入场】新增后回写服务端主键-----------
var (ok, serverId) = await PostJsonForAddAsync(url, payload, ct).ConfigureAwait(false);
if (ok && !string.IsNullOrWhiteSpace(serverId))
{
entry.Id = serverId.Trim();
}
return ok;
//update-end---author:jiangxh ---date:20260731 for【原料入场】新增后回写服务端主键-----------
}
/// <summary>新增接口:解析 Result.OK(msg, id) 的 result 主键。</summary>
private async Task<(bool Ok, string? ServerId)> PostJsonForAddAsync(string url, object body, CancellationToken ct)
{
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode) return (false, null);
try
{
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var codeOk = true;
if (root.TryGetProperty("code", out var codeEl))
codeOk = codeEl.GetInt32() == 200;
else if (root.TryGetProperty("success", out var successEl))
codeOk = successEl.GetBoolean();
if (!codeOk) return (false, null);
string? serverId = null;
if (root.TryGetProperty("result", out var resultEl))
{
serverId = resultEl.ValueKind switch
{
JsonValueKind.String => resultEl.GetString(),
JsonValueKind.Object when resultEl.TryGetProperty("id", out var idEl) => idEl.GetString(),
_ => resultEl.ToString()
};
}
return (true, string.IsNullOrWhiteSpace(serverId) ? null : serverId);
}
catch
{
return (true, null);
}
}
private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslRawMaterialEntry entry, CancellationToken ct)

View File

@@ -17,7 +17,7 @@ public static class HtmlToPdfRenderer
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
Application.Current.Dispatcher.InvokeAsync(async () =>
Application.Current?.Dispatcher.InvokeAsync(async () =>
{
try
{
@@ -30,6 +30,11 @@ public static class HtmlToPdfRenderer
}
});
if (Application.Current == null)
{
tcs.TrySetException(new InvalidOperationException("WPF Application 未初始化,无法生成 PDF"));
}
return tcs.Task;
}

View File

@@ -8,6 +8,7 @@ using System.Globalization;
using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Core.Util;
using YY.Admin.Services.Service;
using YY.Admin.Services.Service.MixerMaterialTareStrategy;
using YY.Admin.Views.RawMaterialEntry;
@@ -25,6 +26,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
private readonly IMixerMaterialService _mixerMaterialService;
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
private readonly ISupplierService _supplierService;
private readonly IWeightRecordService _weightRecordService;
private bool _suppressTareStrategyRefresh;
protected string? _pendingMaterialId;
@@ -212,6 +214,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
IMixerMaterialService mixerMaterialService,
IMixerMaterialTareStrategyService tareStrategyService,
ISupplierService supplierService,
IWeightRecordService weightRecordService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
@@ -220,6 +223,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
_mixerMaterialService = mixerMaterialService;
_tareStrategyService = tareStrategyService;
_supplierService = supplierService;
_weightRecordService = weightRecordService;
SaveCommand = new DelegateCommand(async () => await SaveAsync());
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
ResetCommand = new DelegateCommand(InitializeForAdd);
@@ -468,6 +472,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
return false;
}
//update-begin---author:jiangxh ---date:20260731 for【原料入场】新增/加量前校验磅单剩余净重-----------
if (!await ValidateBillRemainingWeightAsync()) return false;
//update-end---author:jiangxh ---date:20260731 for【原料入场】新增/加量前校验磅单剩余净重-----------
bool ok;
if (IsAddMode)
{
@@ -483,6 +491,69 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
return true;
}
/// <summary>
/// 校验关联磅单剩余可入场量:剩余 = 净重 - 其他已存在入场记录累计(份数×每份重量)。
/// 剩余 ≤ 0 或本次申请量超过剩余时禁止保存。编辑时排除当前记录自身。
/// </summary>
protected async Task<bool> ValidateBillRemainingWeightAsync()
{
if (Entry == null || string.IsNullOrWhiteSpace(Entry.BillNo)) return true;
// 先刷新入场缓存,保证已入场累计与远端一致
await _entryService.PageAsync(1, 1, billNo: Entry.BillNo);
MesXslWeightRecord? weightRecord = null;
if (!string.IsNullOrWhiteSpace(Entry.WeightRecordId))
{
weightRecord = await _weightRecordService.GetByIdAsync(Entry.WeightRecordId);
}
if (weightRecord == null)
{
var page = await _weightRecordService.PageAsync(1, 5, filterBillNo: Entry.BillNo);
weightRecord = page.Records.FirstOrDefault(r =>
string.Equals(r.BillNo, Entry.BillNo, StringComparison.OrdinalIgnoreCase));
}
if (weightRecord?.NetWeight is not { } netWeight)
{
HandyControl.Controls.MessageBox.Warning("关联磅单缺少净重,无法校验剩余可入场量,禁止保存。");
return false;
}
var others = _entryService.GetCachedSnapshot()
.Where(e => !string.IsNullOrWhiteSpace(e.BillNo)
&& string.Equals(e.BillNo, Entry.BillNo, StringComparison.OrdinalIgnoreCase)
&& (string.IsNullOrWhiteSpace(Entry.Id)
|| !string.Equals(e.Id, Entry.Id, StringComparison.OrdinalIgnoreCase)))
.ToList();
var enteredMap = EnteredWeightCalculator.SumByBillNos(others, new[] { Entry.BillNo });
var entered = enteredMap.TryGetValue(Entry.BillNo, out var acc) ? acc : 0d;
var remaining = Math.Round(netWeight - entered, 2);
var requested = EnteredWeightCalculator.SumOneEntry(Entry.TotalPortions, Entry.PortionWeight);
if (requested <= 0d && Entry.TotalWeight is { } tw && tw > 0d)
{
requested = tw;
}
requested = Math.Round(requested, 2);
if (remaining <= 0d)
{
HandyControl.Controls.MessageBox.Warning(
$"关联磅单「{Entry.BillNo}」净重已被已有入场记录扣完(净重 {netWeight:0.##},已入场 {entered:0.##}),不能再生成入场记录。");
return false;
}
if (requested > remaining + 0.009d)
{
HandyControl.Controls.MessageBox.Warning(
$"本次入场重量 {requested:0.##} 超过磅单剩余可入场量 {remaining:0.##}(净重 {netWeight:0.##},已入场 {entered:0.##}),请调整后再保存。");
return false;
}
return true;
}
protected virtual async Task SaveAsync()
{
if (Entry == null) return;

View File

@@ -1,6 +1,6 @@
using HandyControl.Controls;
using HandyControl.Tools.Extension;
using Prism.Events;
using Prism.Navigation;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
@@ -12,7 +12,6 @@ using YY.Admin.Core.Services;
using YY.Admin.Event;
using YY.Admin.Services.Service;
using YY.Admin.Module;
using YY.Admin.Views.RawMaterialEntry;
namespace YY.Admin.ViewModels.RawMaterialEntry;
@@ -80,7 +79,9 @@ public class RawMaterialEntryListViewModel : BaseViewModel
await LoadAsync();
});
AddCommand = new DelegateCommand(OpenAddPage);
EditCommand = new DelegateCommand<MesXslRawMaterialEntry>(async e => await ShowEditDialogAsync(e));
//update-begin---author:jiangxh ---date:20260731 for【原料入场】编辑改为打开与新增相同的操作页-----------
EditCommand = new DelegateCommand<MesXslRawMaterialEntry>(OpenEditPage);
//update-end---author:jiangxh ---date:20260731 for【原料入场】编辑改为打开与新增相同的操作页-----------
DeleteCommand = new DelegateCommand<MesXslRawMaterialEntry>(async e => await DeleteAsync(e));
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } });
@@ -161,18 +162,21 @@ public class RawMaterialEntryListViewModel : BaseViewModel
});
}
private async Task ShowEditDialogAsync(MesXslRawMaterialEntry entry)
//update-begin---author:jiangxh ---date:20260731 for【原料入场】编辑改为打开与新增相同的操作页-----------
private void OpenEditPage(MesXslRawMaterialEntry? entry)
{
if (entry == null) return;
try
if (entry == null || string.IsNullOrWhiteSpace(entry.Id)) return;
var label = string.IsNullOrWhiteSpace(entry.Barcode) ? entry.Id : entry.Barcode;
_eventAggregator.GetEvent<TabSourceSelectedEvent>().Publish(new TabSource
{
var result = await HandyControl.Controls.Dialog.Show<RawMaterialEntryEditDialogView>()
.Initialize<RawMaterialEntryEditDialogViewModel>(vm => vm.InitializeForEdit(entry))
.GetResultAsync<bool>();
if (result) await LoadAsync();
}
catch (Exception ex) { Growl.Error($"打开编辑对话框失败:{ex.Message}"); }
Name = $"编辑原料入场记录 {label}",
Icon = "\ue7ce",
ViewName = "RawMaterialEntryOperationView",
NavigationParameter = new NavigationParameters { { "editEntryId", entry.Id } }
});
}
//update-end---author:jiangxh ---date:20260731 for【原料入场】编辑改为打开与新增相同的操作页-----------
private async Task DeleteAsync(MesXslRawMaterialEntry entry)
{

View File

@@ -1,6 +1,7 @@
using HandyControl.Controls;
using HandyControl.Data;
using Prism.Commands;
using Prism.Navigation;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
@@ -18,9 +19,9 @@ using YY.Admin.Views.RawMaterialEntry;
namespace YY.Admin.ViewModels.RawMaterialEntry;
/// <summary>
/// 「新增原料入场记录独立页面:左侧表单逻辑继承编辑 VM,右侧展示当日入场简要列表并支持选中回填模板
/// 原料入场记录独立操作页(新增/编辑共用):左侧表单,右侧当日入场列表与打印预览
/// </summary>
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel, INavigationAware
{
private const int TodayListFetchSize = 5000;
private const string RawMaterialEntryBizCode = "1900000000000000530";
@@ -30,6 +31,11 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
private readonly IPrintDotService _printDotService;
private readonly IPrintBizTemplateBindService _printBizTemplateBindService;
private readonly IPrintTemplateService _printTemplateService;
//update-begin---author:jiangxh ---date:20260731 for【原料入场】操作页支持导航进入编辑态-----------
private string? _editingEntryId;
/// <summary>是否已由 INavigationAware 处理过导航参数(避免 Loaded 兜底把编辑态冲掉)。</summary>
public bool HasAppliedNavigation { get; private set; }
//update-end---author:jiangxh ---date:20260731 for【原料入场】操作页支持导航进入编辑态-----------
private static readonly JsonSerializerOptions LayoutJsonOpts = new()
{
@@ -98,12 +104,28 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
/// <summary>
/// 「生成原材料卡片」按钮可用条件:
/// 入场记录已保存(有 Id且至少存在一行「未生成卡片 + 份数>0」的明细
/// 已打印态下用户「继续拆码」新增了行,按钮自动重新可用;全部行都已生成卡片时不可用
/// 入场记录已保存(有 Id且至少存在一行「未生成卡片 + 份数>0」的明细
/// 且入场总重尚未被已打印卡片扣完
/// </summary>
public bool CanGenerateCards =>
!string.IsNullOrWhiteSpace(Entry?.Id)
&& SplitCodeDetails.Any(d => !d.HasCard && (d.Portions ?? 0) > 0);
&& SplitCodeDetails.Any(d => !d.HasCard && (d.Portions ?? 0) > 0)
//update-begin---author:jiangxh ---date:20260731 for【原料入场】已打印卡片扣完总重后禁用生成-----------
&& GetEntryRemainingWeightForCards() > 0.009d;
//update-end---author:jiangxh ---date:20260731 for【原料入场】已打印卡片扣完总重后禁用生成-----------
/// <summary>已打印卡片累计重量 = Σ(HasCard 行 份数×每份重量)。</summary>
private double GetPrintedCardsWeight() =>
SplitCodeDetails
.Where(d => d.HasCard)
.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0));
/// <summary>入场总重减去已打印卡片累计后的剩余可打印重量。</summary>
private double GetEntryRemainingWeightForCards()
{
var total = Entry?.TotalWeight ?? 0d;
return Math.Round(total - GetPrintedCardsWeight(), 2);
}
public DelegateCommand ToggleRightPanelCommand { get; }
public DelegateCommand RefreshTodayEntriesCommand { get; }
@@ -244,13 +266,14 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
IMixerMaterialService mixerMaterialService,
IMixerMaterialTareStrategyService tareStrategyService,
ISupplierService supplierService,
IWeightRecordService weightRecordService,
IRawMaterialCardService rawMaterialCardService,
IPrintDotService printDotService,
IPrintBizTemplateBindService printBizTemplateBindService,
IPrintTemplateService printTemplateService,
IContainerExtension container,
IRegionManager regionManager)
: base(entryService, dictSyncService, mixerMaterialService, tareStrategyService, supplierService, container, regionManager)
: base(entryService, dictSyncService, mixerMaterialService, tareStrategyService, supplierService, weightRecordService, container, regionManager)
{
_rawMaterialCardService = rawMaterialCardService;
_printDotService = printDotService;
@@ -314,7 +337,10 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
private void OnSplitDetailHasCardChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.HasCard)
or nameof(RawMaterialSplitDetailItem.Portions))
or nameof(RawMaterialSplitDetailItem.Portions)
//update-begin---author:jiangxh ---date:20260731 for【原料入场】每份重量变化同步刷新可生成状态-----------
or nameof(RawMaterialSplitDetailItem.PortionWeight))
//update-end---author:jiangxh ---date:20260731 for【原料入场】每份重量变化同步刷新可生成状态-----------
{
RaisePropertyChanged(nameof(CanGenerateCards));
RaisePropertyChanged(nameof(CanResplit));
@@ -329,71 +355,175 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
_selectedTodayEntry = null;
RaisePropertyChanged(nameof(SelectedTodayEntry));
_suppressTodaySelectionReaction = false;
//update-begin---author:jiangxh ---date:20260731 for【原料入场】切回新增态时清除编辑导航标记-----------
_editingEntryId = null;
//update-end---author:jiangxh ---date:20260731 for【原料入场】切回新增态时清除编辑导航标记-----------
base.InitializeForAdd();
RaisePropertyChanged(nameof(CanGenerateCards));
}
//update-begin---author:jiangxh ---date:20260731 for【原料入场】操作页支持导航进入编辑态-----------
public void OnNavigatedTo(NavigationContext navigationContext)
{
HasAppliedNavigation = true;
_ = ApplyNavigationAsync(navigationContext.Parameters);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
var parameters = navigationContext.Parameters;
if (parameters.TryGetValue<string>("editEntryId", out var editId) && !string.IsNullOrWhiteSpace(editId))
return string.Equals(_editingEntryId, editId, StringComparison.OrdinalIgnoreCase);
return string.IsNullOrWhiteSpace(_editingEntryId) && IsAddMode;
}
public void OnNavigatedFrom(NavigationContext navigationContext) { }
private async Task ApplyNavigationAsync(INavigationParameters parameters)
{
if (parameters.TryGetValue<string>("editEntryId", out var editId) && !string.IsNullOrWhiteSpace(editId))
{
_editingEntryId = editId;
try
{
IsLoading = true;
var entry = await EntryService.GetByIdAsync(editId);
if (entry == null)
{
Growl.Warning($"未找到入场记录Id={editId}),已切换为新增。");
InitializeForAdd();
return;
}
await ApplyTodayRowToFormAsync(entry);
}
catch (Exception ex)
{
Growl.Error($"加载入场记录失败:{ex.Message}");
InitializeForAdd();
}
finally
{
IsLoading = false;
}
return;
}
_editingEntryId = null;
InitializeForAdd();
}
//update-end---author:jiangxh ---date:20260731 for【原料入场】操作页支持导航进入编辑态-----------
/// <summary>保存后按业务打印绑定拉取模板与 printData直接发送到 PrintDot 打印机(不弹预览窗)。</summary>
private async Task SaveAndPrintAsync()
{
if (IsActionBusy) return;
if (Entry == null) return;
var wasEdit = !IsAddMode;
var barcode = Entry.Barcode;
BeginActionBusy("保存并打印中...");
// 新增模式下:打印失败必须回滚删除刚生成的入场记录,避免“未打印成功却已落库”
var createdInThisAttempt = IsAddMode;
string? createdEntryId = null;
try
{
IsLoading = true;
if (!await PersistEntryCoreAsync()) return;
string? entryId = Entry.Id;
// Persist 后条码/主键可能已回写,再取一次
var barcode = Entry?.Barcode;
string? entryId = Entry?.Id;
if (string.IsNullOrWhiteSpace(entryId) && !string.IsNullOrWhiteSpace(barcode))
{
var page = await EntryService.PageAsync(1, 50, barcode: barcode);
entryId = page.Records
entryId = page.Records?
.FirstOrDefault(e => string.Equals(e.Barcode, barcode, StringComparison.OrdinalIgnoreCase))?.Id
?? page.Records
?? page.Records?
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
.FirstOrDefault()?.Id;
if (!string.IsNullOrWhiteSpace(entryId) && Entry != null)
Entry.Id = entryId;
}
if (createdInThisAttempt)
createdEntryId = entryId;
async Task RollbackCreatedAsync(string reason)
{
if (!createdInThisAttempt || string.IsNullOrWhiteSpace(createdEntryId))
{
HandyControl.Controls.MessageBox.Error(reason);
return;
}
try
{
await EntryService.DeleteAsync(createdEntryId);
if (Entry != null)
{
Entry.Id = null;
Entry.Barcode = null;
Entry.BatchNo = null;
}
Result = false;
await LoadTodayEntriesAsync();
}
catch (Exception delEx)
{
HandyControl.Controls.MessageBox.Error(
$"{reason}\n同时回滚删除入场记录失败{delEx.Message}\n请手动删除刚生成的记录。");
return;
}
HandyControl.Controls.MessageBox.Error($"{reason}\n已撤销本次入场记录未成功打印不会保留数据。");
}
if (string.IsNullOrWhiteSpace(entryId))
{
HandyControl.Controls.MessageBox.Warning("保存成功,但未能解析记录主键,无法打印。请从右侧列表选中该条后再试。");
await RollbackCreatedAsync("保存未能解析记录主键,无法打印。");
return;
}
else
var (templateJson, printDataJson, err) = await EntryService.PrepareNativePrintAsync(entryId);
if (!string.IsNullOrWhiteSpace(err))
{
var (templateJson, printDataJson, err) = await EntryService.PrepareNativePrintAsync(entryId);
if (!string.IsNullOrWhiteSpace(err))
{
HandyControl.Controls.MessageBox.Error($"保存成功,但打印准备失败:{err}");
}
else
{
var selectedPrinterName = SelectedPrinter?.Name?.Trim();
if (string.IsNullOrWhiteSpace(selectedPrinterName))
{
HandyControl.Controls.MessageBox.Warning("保存成功,但未选择打印机。请先选择打印机后再试。");
}
else
{
JsonObject? dataObj = JsonNode.Parse(printDataJson) as JsonObject;
dataObj ??= new JsonObject();
var html = NativePrintRenderService.RenderToHtml(templateJson, dataObj);
var tpl = BuildPrintTemplateForEntry(templateJson);
var pdfBase64 = await HtmlToPdfRenderer.RenderAsync(
html,
tpl.PaperWidthMm ?? 210d,
tpl.PaperHeightMm ?? 297d);
var jobName = string.IsNullOrWhiteSpace(barcode) ? "原料入场记录" : $"原料入场记录-{barcode}";
await _printDotService.PrintAsync(selectedPrinterName, pdfBase64, jobName, 1);
}
}
await RollbackCreatedAsync($"打印准备失败:{err}");
return;
}
if (string.IsNullOrWhiteSpace(templateJson))
{
await RollbackCreatedAsync("打印模板为空,请先配置业务打印绑定。");
return;
}
var selectedPrinterName = SelectedPrinter?.Name?.Trim();
if (string.IsNullOrWhiteSpace(selectedPrinterName))
{
await RollbackCreatedAsync("未选择打印机,请先选择打印机后再试。");
return;
}
JsonObject? dataObj = null;
if (!string.IsNullOrWhiteSpace(printDataJson))
dataObj = JsonNode.Parse(printDataJson) as JsonObject;
dataObj ??= new JsonObject();
var html = NativePrintRenderService.RenderToHtml(templateJson, dataObj);
var tpl = BuildPrintTemplateForEntry(templateJson);
var pdfBase64 = await HtmlToPdfRenderer.RenderAsync(
html,
tpl.PaperWidthMm ?? 210d,
tpl.PaperHeightMm ?? 297d);
if (string.IsNullOrWhiteSpace(pdfBase64))
{
await RollbackCreatedAsync("PDF 生成结果为空,打印失败。");
return;
}
var jobName = string.IsNullOrWhiteSpace(barcode) ? "原料入场记录" : $"原料入场记录-{barcode}";
await _printDotService.PrintAsync(selectedPrinterName, pdfBase64, jobName, 1);
Growl.Success(new GrowlInfo
{
Message = "保存成功",
Message = "保存并打印成功",
ShowDateTime = false,
WaitTime = 1
});
@@ -405,7 +535,33 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
}
catch (Exception ex)
{
HandyControl.Controls.MessageBox.Error($"保存打印失败{ex.Message}");
//update-begin---author:jiangxh ---date:20260731 for【原料入场】保存打印失败时回滚新增记录-----------
if (createdInThisAttempt && !string.IsNullOrWhiteSpace(createdEntryId))
{
try
{
await EntryService.DeleteAsync(createdEntryId);
if (Entry != null)
{
Entry.Id = null;
Entry.Barcode = null;
Entry.BatchNo = null;
}
Result = false;
await LoadTodayEntriesAsync();
HandyControl.Controls.MessageBox.Error($"打印失败:{ex.Message}\n已撤销本次入场记录未成功打印不会保留数据。");
}
catch (Exception delEx)
{
HandyControl.Controls.MessageBox.Error(
$"打印失败:{ex.Message}\n同时回滚删除入场记录失败{delEx.Message}\n请手动删除刚生成的记录。");
}
}
else
{
HandyControl.Controls.MessageBox.Error($"保存或打印失败:{ex.Message}");
}
//update-end---author:jiangxh ---date:20260731 for【原料入场】保存并打印失败时回滚新增记录-----------
}
finally
{
@@ -536,6 +692,9 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
private async Task ApplyTodayRowToFormAsync(MesXslRawMaterialEntry src)
{
// 复用基类 InitializeForEdit保留 Id/Barcode/BatchNo/状态等所有字段,标题自动切到「编辑原料入场记录」
//update-begin---author:jiangxh ---date:20260731 for【原料入场】今日列表选中同步编辑导航标记-----------
_editingEntryId = src.Id;
//update-end---author:jiangxh ---date:20260731 for【原料入场】今日列表选中同步编辑导航标记-----------
base.InitializeForEdit(src);
RaisePropertyChanged(nameof(CanGenerateCards));
@@ -706,17 +865,36 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
}
}
// 总重核对:拆码明细合计 vs 基础资料总重
//update-begin---author:jiangxh ---date:20260731 for【原料入场】已打印卡片扣完总重禁止再打印-----------
{
var entryTotal = Entry.TotalWeight ?? 0d;
var printedWeight = Math.Round(GetPrintedCardsWeight(), 2);
var remaining = Math.Round(entryTotal - printedWeight, 2);
if (remaining <= 0d)
{
HandyControl.Controls.MessageBox.Warning(
$"入场总重 {entryTotal:0.##} 已被已打印原材料卡片总重 {printedWeight:0.##} 扣完,不能再打印原材料卡片。");
return;
}
var pendingWeight = Math.Round(
pendingRows.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0)), 2);
if (pendingWeight > remaining + 0.009d)
{
HandyControl.Controls.MessageBox.Warning(
$"待打印卡片总重 {pendingWeight:0.##} 超过剩余可打印重量 {remaining:0.##}(入场总重 {entryTotal:0.##},已打印 {printedWeight:0.##}),请调整拆码明细后再生成。");
return;
}
}
//update-end---author:jiangxh ---date:20260731 for【原料入场】已打印卡片扣完总重禁止再打印-----------
// 总重核对:拆码明细合计 vs 基础资料总重(剩余不足时上面已硬拦截;此处仅提示拆码合计偏少)
{
var splitTotal = SplitCodeDetails.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0));
var basicTotal = Entry.TotalWeight ?? 0d;
if (Math.Abs(splitTotal - basicTotal) > 0.01d)
if (basicTotal - splitTotal > 0.01d)
{
string hint;
if (splitTotal > basicTotal)
hint = $"本次打印拆码总重 {splitTotal:0.##},超出基础资料总重 {basicTotal:0.##},超出部分是否需要核对?";
else
hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
var hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
var confirm = System.Windows.MessageBox.Show(
hint,
"总重核对",

View File

@@ -1232,7 +1232,7 @@
Style="{StaticResource ButtonPrimary}"
IsEnabled="{Binding IsNotActionBusy}"
Width="168" Margin="0,0,15,0"
ToolTip="保存成功后直接发送到已选打印机(不弹预览窗口)"/>
ToolTip="仅打印成功后保留入场记录;打印失败会自动撤销本次新增"/>
<Button Content="保存"
Command="{Binding SaveCommand}"
Style="{StaticResource ButtonDefault}"

View File

@@ -84,7 +84,11 @@ public partial class RawMaterialEntryOperationView : UserControl
if (DataContext is RawMaterialEntryOperationViewModel vm && !_initialized)
{
vm.InitializeForAdd();
//update-begin---author:jiangxh ---date:20260731 for【原料入场】Loaded 兜底不覆盖导航编辑态-----------
// 有导航参数时由 INavigationAware.OnNavigatedTo 初始化;此处仅无参兜底为新增。
if (!vm.HasAppliedNavigation)
vm.InitializeForAdd();
//update-end---author:jiangxh ---date:20260731 for【原料入场】Loaded 兜底不覆盖导航编辑态-----------
_initialized = true;
_ = vm.LoadTodayEntriesOnFirstShowAsync();
}