桌面端原材料入场记录完善
This commit is contained in:
@@ -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 行为一致,方便桌面端用户自助排查。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
"总重核对",
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user