桌面端原材料入场记录完善
This commit is contained in:
@@ -266,8 +266,43 @@ const (
|
|||||||
printQueuePollInterval = 500 * time.Millisecond
|
printQueuePollInterval = 500 * time.Millisecond
|
||||||
printQueueAppearTimeout = 120 * time.Second
|
printQueueAppearTimeout = 120 * time.Second
|
||||||
printQueueCompleteTimeout = 5 * time.Minute
|
printQueueCompleteTimeout = 5 * time.Minute
|
||||||
|
// 虚拟打印机(Print to PDF 等)常弹「另存为」对话框,且不一定进入 Get-PrintJob 队列
|
||||||
|
virtualPrinterSumatraTimeout = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isVirtualWindowsPrinter 判断是否为虚拟/文件类打印机(队列跟踪不可靠)。
|
||||||
|
func isVirtualWindowsPrinter(printerName string) bool {
|
||||||
|
n := strings.ToLower(strings.TrimSpace(printerName))
|
||||||
|
if n == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
keywords := []string{
|
||||||
|
"microsoft print to pdf",
|
||||||
|
"microsoft xps document writer",
|
||||||
|
"onenote",
|
||||||
|
"fax",
|
||||||
|
"adobe pdf",
|
||||||
|
"pdf24",
|
||||||
|
"foxit pdf",
|
||||||
|
"foxit reader pdf",
|
||||||
|
"bullzip",
|
||||||
|
"dopdf",
|
||||||
|
"cutepdf",
|
||||||
|
"novapdf",
|
||||||
|
"pdfcreator",
|
||||||
|
"print to file",
|
||||||
|
"print to pdf",
|
||||||
|
"pdf",
|
||||||
|
"xps",
|
||||||
|
}
|
||||||
|
for _, k := range keywords {
|
||||||
|
if strings.Contains(n, k) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func getPrintJobs(printerName string) ([]windowsPrintJob, error) {
|
func getPrintJobs(printerName string) ([]windowsPrintJob, error) {
|
||||||
printerName = strings.TrimSpace(printerName)
|
printerName = strings.TrimSpace(printerName)
|
||||||
if printerName == "" {
|
if printerName == "" {
|
||||||
@@ -319,6 +354,11 @@ func getPrintJobIDs(printerName string) (map[int]bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func waitForWindowsPrintCompletion(printerName string, existingIDs map[int]bool, cmdDone <-chan error) error {
|
func waitForWindowsPrintCompletion(printerName string, existingIDs map[int]bool, cmdDone <-chan error) error {
|
||||||
|
// 虚拟打印机:不要求进入 spooler 队列,只等 Sumatra 退出(用户完成另存为)
|
||||||
|
if isVirtualWindowsPrinter(printerName) {
|
||||||
|
return waitForVirtualPrinterPrint(cmdDone)
|
||||||
|
}
|
||||||
|
|
||||||
appearDeadline := time.Now().Add(printQueueAppearTimeout)
|
appearDeadline := time.Now().Add(printQueueAppearTimeout)
|
||||||
completeDeadline := time.Now().Add(printQueueCompleteTimeout)
|
completeDeadline := time.Now().Add(printQueueCompleteTimeout)
|
||||||
|
|
||||||
@@ -345,7 +385,8 @@ func waitForWindowsPrintCompletion(printerName string, existingIDs map[int]bool,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !queued && now.After(appearDeadline) {
|
if !queued && now.After(appearDeadline) {
|
||||||
return fmt.Errorf("print job not queued within %s", printQueueAppearTimeout)
|
// 超时仍未入队:虚拟打印机另存为、或部分驱动不经队列;不再硬失败(避免误报 Printed 0/1)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if queued && now.After(completeDeadline) {
|
if queued && now.After(completeDeadline) {
|
||||||
return fmt.Errorf("print job not completed within %s", printQueueCompleteTimeout)
|
return fmt.Errorf("print job not completed within %s", printQueueCompleteTimeout)
|
||||||
@@ -384,6 +425,29 @@ func waitForWindowsPrintCompletion(printerName string, existingIDs map[int]bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitForVirtualPrinterPrint 虚拟打印机专用:跳过 Get-PrintJob 队列检测。
|
||||||
|
// Microsoft Print to PDF / XPS 等会弹对话框,任务常不进队列,原逻辑会误报 2 分钟超时。
|
||||||
|
func waitForVirtualPrinterPrint(cmdDone <-chan error) error {
|
||||||
|
deadline := time.Now().Add(virtualPrinterSumatraTimeout)
|
||||||
|
ticker := time.NewTicker(printQueuePollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-cmdDone:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sumatra print failed: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
// 对话框可能仍打开或进程未退出;任务多半已交给系统,按成功返回避免业务误报失败
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeJobStatus(status interface{}) []string {
|
func normalizeJobStatus(status interface{}) []string {
|
||||||
switch v := status.(type) {
|
switch v := status.(type) {
|
||||||
case string:
|
case string:
|
||||||
|
|||||||
@@ -188,9 +188,11 @@ The server returns the result of each print:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Windows queue tracking note:**
|
**Windows queue tracking note:**
|
||||||
On Windows, the server waits for the print job to appear in the printer queue and complete before returning `success`.
|
For physical printers, the server waits for the print job to appear in the printer queue and complete before returning `success`.
|
||||||
If the job does not appear within 120 seconds, or does not complete within 5 minutes, it returns `error` with a timeout message.
|
If the job does not appear within 120 seconds, or does not complete within 5 minutes, it returns `error` with a timeout message.
|
||||||
|
|
||||||
|
For virtual printers (e.g. `Microsoft Print to PDF`, `Microsoft XPS Document Writer`, OneNote, common PDF printers), queue polling is skipped. The server only waits for SumatraPDF to exit (up to about 10 minutes) and treats timeout as success, so Save-As dialogs no longer cause false failures.
|
||||||
|
|
||||||
### 3.3 Client Code Example
|
### 3.3 Client Code Example
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
|||||||
@@ -192,8 +192,10 @@ wails dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Windows 队列跟踪说明:**
|
**Windows 队列跟踪说明:**
|
||||||
Windows 下服务端会等待打印任务进入打印队列并完成后才返回 `success`。
|
Windows 下对实体打印机,服务端会等待打印任务进入打印队列并完成后才返回 `success`。
|
||||||
若 120 秒内未入队或 5 分钟内未完成,将返回 `error` 并给出超时提示。
|
若任务最终完成则返回成功;若长时间未入队,现已改为按成功处理(兼容虚拟打印机另存为、部分驱动不经队列),避免误报 `print job not queued`。
|
||||||
|
|
||||||
|
对虚拟打印机(如 `Microsoft Print to PDF`、`Microsoft XPS Document Writer`、OneNote、名称含 pdf/xps 的打印机等)会跳过队列检测,仅等待 Sumatra 退出(最长约 10 分钟)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1401,3 +1401,23 @@ jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.
|
|||||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue
|
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue
|
||||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.api.ts
|
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.api.ts
|
||||||
-- author:GHT---date:20260730--for: <20><>XSLMES-20260730-DING-PULL<4C><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̨<EFBFBD><CCA8><EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD>ܼ<EFBFBD><DCBC><EFBFBD> ---
|
-- author:GHT---date:20260730--for: <20><>XSLMES-20260730-DING-PULL<4C><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̨<EFBFBD><CCA8><EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD>ܼ<EFBFBD><DCBC><EFBFBD> ---
|
||||||
|
|
||||||
|
-- author:jiangxh---date:20260731--for: <20><>ԭ<EFBFBD><D4AD><EFBFBD>볡<EFBFBD><EBB3A1>anon<6F><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˱<EFBFBD><CBB1>沢<EFBFBD><E6B2A2>ӡ<EFBFBD><D3A1>дId<49><64><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ---
|
||||||
|
jeecg-boot/.../MesXslDesktopAnonController.java
|
||||||
|
yy-admin-master/.../RawMaterialEntryService.cs
|
||||||
|
yy-admin-master/.../RawMaterialEntryOperationViewModel.cs
|
||||||
|
yy-admin-master/.../PrintDotService.cs
|
||||||
|
yy-admin-master/.../HtmlToPdfRenderer.cs
|
||||||
|
|
||||||
|
-- author:jiangxh---date:20260731--for: <20><>ԭ<EFBFBD><D4AD><EFBFBD>볡<EFBFBD><EBB3A1><EFBFBD><EFBFBD><EFBFBD>沢<EFBFBD><E6B2A2>ӡʧ<D3A1>ܻع<DCBB><D8B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ؿ<EFBFBD><D8BF><EFBFBD><EFBFBD><EFBFBD>ֹ<EFBFBD><D6B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ---
|
||||||
|
yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs
|
||||||
|
yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs
|
||||||
|
yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml
|
||||||
|
|
||||||
|
-- author:jiangxh---date:20260731--for: <20><>ԭ<EFBFBD><D4AD><EFBFBD>볡<EFBFBD><EBB3A1><EFBFBD>༭<EFBFBD><E0BCAD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD>IJ<EFBFBD><C4B2><EFBFBD>ҳ ---
|
||||||
|
yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryListViewModel.cs
|
||||||
|
yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs
|
||||||
|
yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryOperationView.xaml.cs
|
||||||
|
|
||||||
|
-- author:jiangxh---date:20260731--for: <20><>ԭ<EFBFBD><D4AD><EFBFBD>볡<EFBFBD><EBB3A1><EFBFBD>Ѵ<EFBFBD>ӡ<EFBFBD><D3A1>Ƭ<EFBFBD><C6AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ؽ<EFBFBD>ֹ<EFBFBD><D6B9><EFBFBD><EFBFBD><EFBFBD>ɿ<EFBFBD>Ƭ ---
|
||||||
|
yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs
|
||||||
|
|||||||
@@ -518,7 +518,10 @@ public class MesXslDesktopAnonController {
|
|||||||
}
|
}
|
||||||
rawMaterialEntryService.save(entity);
|
rawMaterialEntryService.save(entity);
|
||||||
stompNotify.publishRawMaterialEntryChanged("add", entity.getId());
|
stompNotify.publishRawMaterialEntryChanged("add", entity.getId());
|
||||||
return Result.OK("添加成功!");
|
//update-begin---author:jiangxh ---date:20260731 for:【原料入场】anon新增返回主键供桌面端保存并打印-----------
|
||||||
|
// result 带回主键,桌面端保存并打印时无需再按条码回查
|
||||||
|
return Result.OK("添加成功!", entity.getId());
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】anon新增返回主键供桌面端保存并打印-----------
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "原料入场记录-免密编辑")
|
@Operation(summary = "原料入场记录-免密编辑")
|
||||||
|
|||||||
@@ -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)
|
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: 前缀
|
// 去掉 data: 前缀
|
||||||
var content = pdfBase64.Trim();
|
var content = pdfBase64.Trim();
|
||||||
var comma = content.IndexOf(',');
|
var comma = content.IndexOf(',');
|
||||||
if (content.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && comma >= 0)
|
if (content.StartsWith("data:", StringComparison.OrdinalIgnoreCase) && comma >= 0)
|
||||||
content = content[(comma + 1)..];
|
content = content[(comma + 1)..];
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
throw new InvalidOperationException("打印内容为空,PDF 生成失败");
|
||||||
|
|
||||||
var payload = new
|
var payload = new
|
||||||
{
|
{
|
||||||
@@ -87,14 +94,46 @@ public class PrintDotService : IPrintDotService
|
|||||||
{
|
{
|
||||||
var response = await ReceiveTextAsync(ws, cts.Token);
|
var response = await ReceiveTextAsync(ws, cts.Token);
|
||||||
var resDoc = JsonNode.Parse(response);
|
var resDoc = JsonNode.Parse(response);
|
||||||
var status = resDoc?["status"]?.GetValue<string>();
|
var status = TryReadJsonString(resDoc?["status"]);
|
||||||
if (status == null) continue;
|
if (string.IsNullOrWhiteSpace(status)) continue;
|
||||||
if (status == "success") return;
|
if (string.Equals(status, "success", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
var rawMsg = resDoc?["message"]?.GetValue<string>() ?? "PrintDot 打印失败";
|
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));
|
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>
|
/// <summary>
|
||||||
/// 将 PrintDot 返回的部分英文错误转换为带本地处理步骤的中文提示。
|
/// 将 PrintDot 返回的部分英文错误转换为带本地处理步骤的中文提示。
|
||||||
/// 与 web 端 printDotBridge.ts::enhancePrintDotErrorMessage 行为一致,方便桌面端用户自助排查。
|
/// 与 web 端 printDotBridge.ts::enhancePrintDotErrorMessage 行为一致,方便桌面端用户自助排查。
|
||||||
|
|||||||
@@ -159,7 +159,14 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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 });
|
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);
|
UpsertLocalCache(local);
|
||||||
return true;
|
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)
|
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 url = $"{BaseUrl}/xslmes/mesXslRawMaterialEntry/anon/add?tenantId={DefaultTenantId}";
|
||||||
var payload = Clone(entry);
|
var payload = Clone(entry);
|
||||||
if (IsLocalTempId(payload.Id)) payload.Id = null;
|
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)
|
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);
|
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
Application.Current.Dispatcher.InvokeAsync(async () =>
|
Application.Current?.Dispatcher.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -30,6 +30,11 @@ public static class HtmlToPdfRenderer
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (Application.Current == null)
|
||||||
|
{
|
||||||
|
tcs.TrySetException(new InvalidOperationException("WPF Application 未初始化,无法生成 PDF"));
|
||||||
|
}
|
||||||
|
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Globalization;
|
|||||||
using YY.Admin.Core;
|
using YY.Admin.Core;
|
||||||
using YY.Admin.Core.Entity;
|
using YY.Admin.Core.Entity;
|
||||||
using YY.Admin.Core.Services;
|
using YY.Admin.Core.Services;
|
||||||
|
using YY.Admin.Core.Util;
|
||||||
using YY.Admin.Services.Service;
|
using YY.Admin.Services.Service;
|
||||||
using YY.Admin.Services.Service.MixerMaterialTareStrategy;
|
using YY.Admin.Services.Service.MixerMaterialTareStrategy;
|
||||||
using YY.Admin.Views.RawMaterialEntry;
|
using YY.Admin.Views.RawMaterialEntry;
|
||||||
@@ -25,6 +26,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
|||||||
private readonly IMixerMaterialService _mixerMaterialService;
|
private readonly IMixerMaterialService _mixerMaterialService;
|
||||||
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
|
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
|
||||||
private readonly ISupplierService _supplierService;
|
private readonly ISupplierService _supplierService;
|
||||||
|
private readonly IWeightRecordService _weightRecordService;
|
||||||
private bool _suppressTareStrategyRefresh;
|
private bool _suppressTareStrategyRefresh;
|
||||||
protected string? _pendingMaterialId;
|
protected string? _pendingMaterialId;
|
||||||
|
|
||||||
@@ -212,6 +214,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
|||||||
IMixerMaterialService mixerMaterialService,
|
IMixerMaterialService mixerMaterialService,
|
||||||
IMixerMaterialTareStrategyService tareStrategyService,
|
IMixerMaterialTareStrategyService tareStrategyService,
|
||||||
ISupplierService supplierService,
|
ISupplierService supplierService,
|
||||||
|
IWeightRecordService weightRecordService,
|
||||||
IContainerExtension container,
|
IContainerExtension container,
|
||||||
IRegionManager regionManager) : base(container, regionManager)
|
IRegionManager regionManager) : base(container, regionManager)
|
||||||
{
|
{
|
||||||
@@ -220,6 +223,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
|||||||
_mixerMaterialService = mixerMaterialService;
|
_mixerMaterialService = mixerMaterialService;
|
||||||
_tareStrategyService = tareStrategyService;
|
_tareStrategyService = tareStrategyService;
|
||||||
_supplierService = supplierService;
|
_supplierService = supplierService;
|
||||||
|
_weightRecordService = weightRecordService;
|
||||||
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
SaveCommand = new DelegateCommand(async () => await SaveAsync());
|
||||||
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
|
||||||
ResetCommand = new DelegateCommand(InitializeForAdd);
|
ResetCommand = new DelegateCommand(InitializeForAdd);
|
||||||
@@ -468,6 +472,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:jiangxh ---date:20260731 for:【原料入场】新增/加量前校验磅单剩余净重-----------
|
||||||
|
if (!await ValidateBillRemainingWeightAsync()) return false;
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】新增/加量前校验磅单剩余净重-----------
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
if (IsAddMode)
|
if (IsAddMode)
|
||||||
{
|
{
|
||||||
@@ -483,6 +491,69 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
|
|||||||
return true;
|
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()
|
protected virtual async Task SaveAsync()
|
||||||
{
|
{
|
||||||
if (Entry == null) return;
|
if (Entry == null) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using HandyControl.Controls;
|
using HandyControl.Controls;
|
||||||
using HandyControl.Tools.Extension;
|
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
|
using Prism.Navigation;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
@@ -12,7 +12,6 @@ using YY.Admin.Core.Services;
|
|||||||
using YY.Admin.Event;
|
using YY.Admin.Event;
|
||||||
using YY.Admin.Services.Service;
|
using YY.Admin.Services.Service;
|
||||||
using YY.Admin.Module;
|
using YY.Admin.Module;
|
||||||
using YY.Admin.Views.RawMaterialEntry;
|
|
||||||
|
|
||||||
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||||
|
|
||||||
@@ -80,7 +79,9 @@ public class RawMaterialEntryListViewModel : BaseViewModel
|
|||||||
await LoadAsync();
|
await LoadAsync();
|
||||||
});
|
});
|
||||||
AddCommand = new DelegateCommand(OpenAddPage);
|
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));
|
DeleteCommand = new DelegateCommand<MesXslRawMaterialEntry>(async e => await DeleteAsync(e));
|
||||||
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
|
PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } });
|
||||||
NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { 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;
|
if (entry == null || string.IsNullOrWhiteSpace(entry.Id)) return;
|
||||||
try
|
|
||||||
|
var label = string.IsNullOrWhiteSpace(entry.Barcode) ? entry.Id : entry.Barcode;
|
||||||
|
_eventAggregator.GetEvent<TabSourceSelectedEvent>().Publish(new TabSource
|
||||||
{
|
{
|
||||||
var result = await HandyControl.Controls.Dialog.Show<RawMaterialEntryEditDialogView>()
|
Name = $"编辑原料入场记录 {label}",
|
||||||
.Initialize<RawMaterialEntryEditDialogViewModel>(vm => vm.InitializeForEdit(entry))
|
Icon = "\ue7ce",
|
||||||
.GetResultAsync<bool>();
|
ViewName = "RawMaterialEntryOperationView",
|
||||||
if (result) await LoadAsync();
|
NavigationParameter = new NavigationParameters { { "editEntryId", entry.Id } }
|
||||||
}
|
});
|
||||||
catch (Exception ex) { Growl.Error($"打开编辑对话框失败:{ex.Message}"); }
|
|
||||||
}
|
}
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】编辑改为打开与新增相同的操作页-----------
|
||||||
|
|
||||||
private async Task DeleteAsync(MesXslRawMaterialEntry entry)
|
private async Task DeleteAsync(MesXslRawMaterialEntry entry)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using HandyControl.Controls;
|
using HandyControl.Controls;
|
||||||
using HandyControl.Data;
|
using HandyControl.Data;
|
||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
|
using Prism.Navigation;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -18,9 +19,9 @@ using YY.Admin.Views.RawMaterialEntry;
|
|||||||
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
namespace YY.Admin.ViewModels.RawMaterialEntry;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 「新增原料入场记录」独立页面:左侧表单逻辑继承编辑 VM,右侧展示当日入场简要列表并支持选中回填模板。
|
/// 原料入场记录独立操作页(新增/编辑共用):左侧表单,右侧当日入场列表与打印预览。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel
|
public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogViewModel, INavigationAware
|
||||||
{
|
{
|
||||||
private const int TodayListFetchSize = 5000;
|
private const int TodayListFetchSize = 5000;
|
||||||
private const string RawMaterialEntryBizCode = "1900000000000000530";
|
private const string RawMaterialEntryBizCode = "1900000000000000530";
|
||||||
@@ -30,6 +31,11 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
private readonly IPrintDotService _printDotService;
|
private readonly IPrintDotService _printDotService;
|
||||||
private readonly IPrintBizTemplateBindService _printBizTemplateBindService;
|
private readonly IPrintBizTemplateBindService _printBizTemplateBindService;
|
||||||
private readonly IPrintTemplateService _printTemplateService;
|
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()
|
private static readonly JsonSerializerOptions LayoutJsonOpts = new()
|
||||||
{
|
{
|
||||||
@@ -98,12 +104,28 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 「生成原材料卡片」按钮可用条件:
|
/// 「生成原材料卡片」按钮可用条件:
|
||||||
/// 入场记录已保存(有 Id)且至少存在一行「未生成卡片 + 份数>0」的明细。
|
/// 入场记录已保存(有 Id)且至少存在一行「未生成卡片 + 份数>0」的明细,
|
||||||
/// 已打印态下用户「继续拆码」新增了行,按钮自动重新可用;全部行都已生成卡片时不可用。
|
/// 且入场总重尚未被已打印卡片扣完。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CanGenerateCards =>
|
public bool CanGenerateCards =>
|
||||||
!string.IsNullOrWhiteSpace(Entry?.Id)
|
!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 ToggleRightPanelCommand { get; }
|
||||||
public DelegateCommand RefreshTodayEntriesCommand { get; }
|
public DelegateCommand RefreshTodayEntriesCommand { get; }
|
||||||
@@ -244,13 +266,14 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
IMixerMaterialService mixerMaterialService,
|
IMixerMaterialService mixerMaterialService,
|
||||||
IMixerMaterialTareStrategyService tareStrategyService,
|
IMixerMaterialTareStrategyService tareStrategyService,
|
||||||
ISupplierService supplierService,
|
ISupplierService supplierService,
|
||||||
|
IWeightRecordService weightRecordService,
|
||||||
IRawMaterialCardService rawMaterialCardService,
|
IRawMaterialCardService rawMaterialCardService,
|
||||||
IPrintDotService printDotService,
|
IPrintDotService printDotService,
|
||||||
IPrintBizTemplateBindService printBizTemplateBindService,
|
IPrintBizTemplateBindService printBizTemplateBindService,
|
||||||
IPrintTemplateService printTemplateService,
|
IPrintTemplateService printTemplateService,
|
||||||
IContainerExtension container,
|
IContainerExtension container,
|
||||||
IRegionManager regionManager)
|
IRegionManager regionManager)
|
||||||
: base(entryService, dictSyncService, mixerMaterialService, tareStrategyService, supplierService, container, regionManager)
|
: base(entryService, dictSyncService, mixerMaterialService, tareStrategyService, supplierService, weightRecordService, container, regionManager)
|
||||||
{
|
{
|
||||||
_rawMaterialCardService = rawMaterialCardService;
|
_rawMaterialCardService = rawMaterialCardService;
|
||||||
_printDotService = printDotService;
|
_printDotService = printDotService;
|
||||||
@@ -314,7 +337,10 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
private void OnSplitDetailHasCardChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void OnSplitDetailHasCardChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.HasCard)
|
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(CanGenerateCards));
|
||||||
RaisePropertyChanged(nameof(CanResplit));
|
RaisePropertyChanged(nameof(CanResplit));
|
||||||
@@ -329,55 +355,156 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
_selectedTodayEntry = null;
|
_selectedTodayEntry = null;
|
||||||
RaisePropertyChanged(nameof(SelectedTodayEntry));
|
RaisePropertyChanged(nameof(SelectedTodayEntry));
|
||||||
_suppressTodaySelectionReaction = false;
|
_suppressTodaySelectionReaction = false;
|
||||||
|
//update-begin---author:jiangxh ---date:20260731 for:【原料入场】切回新增态时清除编辑导航标记-----------
|
||||||
|
_editingEntryId = null;
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】切回新增态时清除编辑导航标记-----------
|
||||||
base.InitializeForAdd();
|
base.InitializeForAdd();
|
||||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
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>
|
/// <summary>保存后按业务打印绑定拉取模板与 printData,直接发送到 PrintDot 打印机(不弹预览窗)。</summary>
|
||||||
private async Task SaveAndPrintAsync()
|
private async Task SaveAndPrintAsync()
|
||||||
{
|
{
|
||||||
if (IsActionBusy) return;
|
if (IsActionBusy) return;
|
||||||
if (Entry == null) return;
|
if (Entry == null) return;
|
||||||
var wasEdit = !IsAddMode;
|
|
||||||
var barcode = Entry.Barcode;
|
|
||||||
BeginActionBusy("保存并打印中...");
|
BeginActionBusy("保存并打印中...");
|
||||||
|
// 新增模式下:打印失败必须回滚删除刚生成的入场记录,避免“未打印成功却已落库”
|
||||||
|
var createdInThisAttempt = IsAddMode;
|
||||||
|
string? createdEntryId = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
if (!await PersistEntryCoreAsync()) return;
|
if (!await PersistEntryCoreAsync()) return;
|
||||||
|
|
||||||
string? entryId = Entry.Id;
|
// Persist 后条码/主键可能已回写,再取一次
|
||||||
|
var barcode = Entry?.Barcode;
|
||||||
|
string? entryId = Entry?.Id;
|
||||||
if (string.IsNullOrWhiteSpace(entryId) && !string.IsNullOrWhiteSpace(barcode))
|
if (string.IsNullOrWhiteSpace(entryId) && !string.IsNullOrWhiteSpace(barcode))
|
||||||
{
|
{
|
||||||
var page = await EntryService.PageAsync(1, 50, barcode: 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
|
.FirstOrDefault(e => string.Equals(e.Barcode, barcode, StringComparison.OrdinalIgnoreCase))?.Id
|
||||||
?? page.Records
|
?? page.Records?
|
||||||
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
|
.OrderByDescending(e => e.EntryTime ?? e.CreateTime ?? DateTime.MinValue)
|
||||||
.FirstOrDefault()?.Id;
|
.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))
|
if (string.IsNullOrWhiteSpace(entryId))
|
||||||
{
|
{
|
||||||
HandyControl.Controls.MessageBox.Warning("保存成功,但未能解析记录主键,无法打印。请从右侧列表选中该条后再试。");
|
await RollbackCreatedAsync("保存后未能解析记录主键,无法打印。");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var (templateJson, printDataJson, err) = await EntryService.PrepareNativePrintAsync(entryId);
|
var (templateJson, printDataJson, err) = await EntryService.PrepareNativePrintAsync(entryId);
|
||||||
if (!string.IsNullOrWhiteSpace(err))
|
if (!string.IsNullOrWhiteSpace(err))
|
||||||
{
|
{
|
||||||
HandyControl.Controls.MessageBox.Error($"保存成功,但打印准备失败:{err}");
|
await RollbackCreatedAsync($"打印准备失败:{err}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
if (string.IsNullOrWhiteSpace(templateJson))
|
||||||
{
|
{
|
||||||
|
await RollbackCreatedAsync("打印模板为空,请先配置业务打印绑定。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var selectedPrinterName = SelectedPrinter?.Name?.Trim();
|
var selectedPrinterName = SelectedPrinter?.Name?.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(selectedPrinterName))
|
if (string.IsNullOrWhiteSpace(selectedPrinterName))
|
||||||
{
|
{
|
||||||
HandyControl.Controls.MessageBox.Warning("保存成功,但未选择打印机。请先选择打印机后再试。");
|
await RollbackCreatedAsync("未选择打印机,请先选择打印机后再试。");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
JsonObject? dataObj = null;
|
||||||
JsonObject? dataObj = JsonNode.Parse(printDataJson) as JsonObject;
|
if (!string.IsNullOrWhiteSpace(printDataJson))
|
||||||
|
dataObj = JsonNode.Parse(printDataJson) as JsonObject;
|
||||||
dataObj ??= new JsonObject();
|
dataObj ??= new JsonObject();
|
||||||
var html = NativePrintRenderService.RenderToHtml(templateJson, dataObj);
|
var html = NativePrintRenderService.RenderToHtml(templateJson, dataObj);
|
||||||
var tpl = BuildPrintTemplateForEntry(templateJson);
|
var tpl = BuildPrintTemplateForEntry(templateJson);
|
||||||
@@ -385,15 +512,18 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
html,
|
html,
|
||||||
tpl.PaperWidthMm ?? 210d,
|
tpl.PaperWidthMm ?? 210d,
|
||||||
tpl.PaperHeightMm ?? 297d);
|
tpl.PaperHeightMm ?? 297d);
|
||||||
|
if (string.IsNullOrWhiteSpace(pdfBase64))
|
||||||
|
{
|
||||||
|
await RollbackCreatedAsync("PDF 生成结果为空,打印失败。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var jobName = string.IsNullOrWhiteSpace(barcode) ? "原料入场记录" : $"原料入场记录-{barcode}";
|
var jobName = string.IsNullOrWhiteSpace(barcode) ? "原料入场记录" : $"原料入场记录-{barcode}";
|
||||||
await _printDotService.PrintAsync(selectedPrinterName, pdfBase64, jobName, 1);
|
await _printDotService.PrintAsync(selectedPrinterName, pdfBase64, jobName, 1);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Growl.Success(new GrowlInfo
|
Growl.Success(new GrowlInfo
|
||||||
{
|
{
|
||||||
Message = "保存成功",
|
Message = "保存并打印成功",
|
||||||
ShowDateTime = false,
|
ShowDateTime = false,
|
||||||
WaitTime = 1
|
WaitTime = 1
|
||||||
});
|
});
|
||||||
@@ -404,9 +534,35 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
InitializeForAdd();
|
InitializeForAdd();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//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}");
|
HandyControl.Controls.MessageBox.Error($"保存或打印失败:{ex.Message}");
|
||||||
}
|
}
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】保存并打印失败时回滚新增记录-----------
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
@@ -536,6 +692,9 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView
|
|||||||
private async Task ApplyTodayRowToFormAsync(MesXslRawMaterialEntry src)
|
private async Task ApplyTodayRowToFormAsync(MesXslRawMaterialEntry src)
|
||||||
{
|
{
|
||||||
// 复用基类 InitializeForEdit:保留 Id/Barcode/BatchNo/状态等所有字段,标题自动切到「编辑原料入场记录」
|
// 复用基类 InitializeForEdit:保留 Id/Barcode/BatchNo/状态等所有字段,标题自动切到「编辑原料入场记录」
|
||||||
|
//update-begin---author:jiangxh ---date:20260731 for:【原料入场】今日列表选中同步编辑导航标记-----------
|
||||||
|
_editingEntryId = src.Id;
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】今日列表选中同步编辑导航标记-----------
|
||||||
base.InitializeForEdit(src);
|
base.InitializeForEdit(src);
|
||||||
RaisePropertyChanged(nameof(CanGenerateCards));
|
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 splitTotal = SplitCodeDetails.Sum(d => (d.PortionWeight ?? 0d) * (d.Portions ?? 0));
|
||||||
var basicTotal = Entry.TotalWeight ?? 0d;
|
var basicTotal = Entry.TotalWeight ?? 0d;
|
||||||
if (Math.Abs(splitTotal - basicTotal) > 0.01d)
|
if (basicTotal - splitTotal > 0.01d)
|
||||||
{
|
{
|
||||||
string hint;
|
var hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
|
||||||
if (splitTotal > basicTotal)
|
|
||||||
hint = $"本次打印拆码总重 {splitTotal:0.##},超出基础资料总重 {basicTotal:0.##},超出部分是否需要核对?";
|
|
||||||
else
|
|
||||||
hint = $"本次打印拆码总重 {splitTotal:0.##},少于基础资料总重 {basicTotal:0.##},剩余部分将无法继续拆码,是否继续?";
|
|
||||||
var confirm = System.Windows.MessageBox.Show(
|
var confirm = System.Windows.MessageBox.Show(
|
||||||
hint,
|
hint,
|
||||||
"总重核对",
|
"总重核对",
|
||||||
|
|||||||
@@ -1232,7 +1232,7 @@
|
|||||||
Style="{StaticResource ButtonPrimary}"
|
Style="{StaticResource ButtonPrimary}"
|
||||||
IsEnabled="{Binding IsNotActionBusy}"
|
IsEnabled="{Binding IsNotActionBusy}"
|
||||||
Width="168" Margin="0,0,15,0"
|
Width="168" Margin="0,0,15,0"
|
||||||
ToolTip="保存成功后直接发送到已选打印机(不弹预览窗口)"/>
|
ToolTip="仅打印成功后保留入场记录;打印失败会自动撤销本次新增"/>
|
||||||
<Button Content="保存"
|
<Button Content="保存"
|
||||||
Command="{Binding SaveCommand}"
|
Command="{Binding SaveCommand}"
|
||||||
Style="{StaticResource ButtonDefault}"
|
Style="{StaticResource ButtonDefault}"
|
||||||
|
|||||||
@@ -84,7 +84,11 @@ public partial class RawMaterialEntryOperationView : UserControl
|
|||||||
|
|
||||||
if (DataContext is RawMaterialEntryOperationViewModel vm && !_initialized)
|
if (DataContext is RawMaterialEntryOperationViewModel vm && !_initialized)
|
||||||
{
|
{
|
||||||
|
//update-begin---author:jiangxh ---date:20260731 for:【原料入场】Loaded 兜底不覆盖导航编辑态-----------
|
||||||
|
// 有导航参数时由 INavigationAware.OnNavigatedTo 初始化;此处仅无参兜底为新增。
|
||||||
|
if (!vm.HasAppliedNavigation)
|
||||||
vm.InitializeForAdd();
|
vm.InitializeForAdd();
|
||||||
|
//update-end---author:jiangxh ---date:20260731 for:【原料入场】Loaded 兜底不覆盖导航编辑态-----------
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
_ = vm.LoadTodayEntriesOnFirstShowAsync();
|
_ = vm.LoadTodayEntriesOnFirstShowAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user