From fef7d25e3cbc01fb3cc73fac90aaf34de5904a0a Mon Sep 17 00:00:00 2001 From: geht <2947093423@qq.com> Date: Tue, 2 Jun 2026 16:28:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=86=E7=82=BC=E7=89=A9?= =?UTF-8?q?=E6=96=99=E7=9A=AE=E9=87=8D=E7=AD=96=E7=95=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E6=8B=AC=E7=9B=B8=E5=85=B3=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E3=80=81=E6=9C=8D=E5=8A=A1=E3=80=81=E6=8E=A7=E5=88=B6=E5=99=A8?= =?UTF-8?q?=E5=8F=8A=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=94=AF=E6=8C=81=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E7=AB=AF=E5=85=8D=E5=AF=86CRUD=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=89=93=E5=8D=B0=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=B8=8E=E5=8E=9F=E6=96=99=E5=85=A5=E5=9C=BA=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E7=9A=84=E8=A1=8D=E7=94=9F=E5=AD=97=E6=AE=B5=E5=A1=AB=E5=85=85?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XSLPrintDot/backend_windows.go | 9 + .../org/jeecg/config/shiro/ShiroConfig.java | 4 + .../jeecg-module-xslmes/doc/代码修改日志 | 65 ++ .../MesXslDesktopAnonController.java | 99 ++- ...slMixerMaterialTareStrategyController.java | 151 ++++ .../MesXslWeightRecordController.java | 30 +- .../MesXslMixerMaterialTareStrategy.java | 107 +++ .../xslmes/entity/MesXslRawMaterialCard.java | 8 + .../xslmes/entity/MesXslRawMaterialEntry.java | 15 + .../MesXslRawMaterialWorkshopRemain.java | 8 + .../xslmes/entity/MesXslWeightRecord.java | 21 +- ...MesXslMixerMaterialTareStrategyMapper.java | 11 + ...esXslMixerMaterialTareStrategyService.java | 22 + .../IMesXslRawMaterialEntryService.java | 14 + .../service/MesXslStompNotifyService.java | 8 + ...lMixerMaterialTareStrategyServiceImpl.java | 107 +++ .../MesXslRawMaterialEntryServiceImpl.java | 70 ++ .../jeecg-system-biz/docs/代码修改日志 | 31 + ...__mes_xsl_mixer_material_tare_strategy.sql | 121 +++ ...sl_mixer_material_tare_strategy_fields.sql | 9 + ...mes_xsl_raw_material_entry_tare_fields.sql | 8 + ..._mes_xsl_raw_material_card_tare_fields.sql | 6 + .../MesXslMixerMaterialTareStrategy.api.ts | 30 + .../MesXslMixerMaterialTareStrategy.data.ts | 153 ++++ .../MesXslMixerMaterialTareStrategyList.vue | 153 ++++ .../MesXslMixerMaterialTareStrategyModal.vue | 199 +++++ .../MesXslRawMaterialCard.data.ts | 54 +- .../MesXslRawMaterialEntry.data.ts | 28 +- .../MesXslRawMaterialWorkshopRemain.data.ts | 14 +- .../MesXslWeightRecord.data.ts | 24 + jeecgboot-vue3/tsconfig.json | 1 + scan_tare_strategy.json | 3 + .../MixerMaterialTareStrategyChangedEvent.cs | 11 + .../IMixerMaterialTareStrategyService.cs | 28 + .../Entity/MesXslMixerMaterialTareStrategy.cs | 31 + .../Entity/MesXslRawMaterialCard.cs | 4 + .../Entity/MesXslRawMaterialEntry.cs | 4 + .../YY.Admin.Core/Entity/MesXslUnit.cs | 9 + .../Entity/MesXslWeightRecord.cs | 11 + .../YY.Admin.Core/SeedData/SysMenuSeedData.cs | 2 + .../SeedData/SysTenantMenuSeedData.cs | 1 + .../YY.Admin.Core/SqlSugar/SqlSugarSetup.cs | 102 +++ .../Util/CargoTareWeightCalculator.cs | 49 ++ .../MixerMaterialTareStrategyMatcher.cs | 80 ++ .../MixerMaterialTareStrategyService.cs | 712 ++++++++++++++++++ ...ixerMaterialTareStrategySyncCoordinator.cs | 72 ++ .../RawMaterialCard/RawMaterialCardService.cs | 2 + .../RawMaterialEntryService.cs | 9 +- .../WeightRecord/WeightRecordService.cs | 39 +- .../Hubs/StompWebSocketService.cs | 4 + .../YY.Admin/Module/NavigationExtensions.cs | 3 + yy-admin-master/YY.Admin/Module/SyncModule.cs | 6 + .../ViewModels/Control/MenuTreeViewModel.cs | 5 + ...MaterialTareStrategyEditDialogViewModel.cs | 295 ++++++++ .../MixerMaterialTareStrategyListViewModel.cs | 221 ++++++ .../RawMaterialCardEditDialogViewModel.cs | 2 + .../RawMaterialEntryEditDialogViewModel.cs | 471 ++++++++++-- .../RawMaterialEntryOperationViewModel.cs | 18 +- .../TareStrategyPickerDialogViewModel.cs | 110 +++ ...xerMaterialTareStrategyEditDialogView.xaml | 163 ++++ ...MaterialTareStrategyEditDialogView.xaml.cs | 11 + .../MixerMaterialTareStrategyListView.xaml | 138 ++++ .../MixerMaterialTareStrategyListView.xaml.cs | 11 + .../Views/Print/PrintPreviewWindow.xaml.cs | 12 +- .../RawMaterialCardListView.xaml | 2 + .../RawMaterialEntryListView.xaml | 1 + .../RawMaterialEntryOperationView.xaml | 153 +++- .../TareStrategyPickerDialogView.xaml | 48 ++ .../TareStrategyPickerDialogView.xaml.cs | 9 + .../WeightRecordPickerDialogView.xaml | 1 + .../WeightRecord/WeightRecordListView.xaml | 2 + yy-admin-master/YY.Admin/menu_fix.csx | 44 +- .../scripts/MenuFixTemp/MenuFixTemp.csproj | 11 + .../scripts/MenuFixTemp/Program.cs | 37 + .../scripts/ensure_tare_strategy_menu.ps1 | 50 ++ 75 files changed, 4407 insertions(+), 170 deletions(-) create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerMaterialTareStrategyController.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixerMaterialTareStrategyMapper.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerMaterialTareStrategyService.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_mixer_material_tare_strategy.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_118__mes_xsl_mixer_material_tare_strategy_fields.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_119__mes_xsl_raw_material_entry_tare_fields.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_120__mes_xsl_raw_material_card_tare_fields.sql create mode 100644 jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.api.ts create mode 100644 jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts create mode 100644 jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategyList.vue create mode 100644 jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue create mode 100644 scan_tare_strategy.json create mode 100644 yy-admin-master/YY.Admin.Core/Core/Events/MixerMaterialTareStrategyChangedEvent.cs create mode 100644 yy-admin-master/YY.Admin.Core/Core/Services/IMixerMaterialTareStrategyService.cs create mode 100644 yy-admin-master/YY.Admin.Core/Entity/MesXslMixerMaterialTareStrategy.cs create mode 100644 yy-admin-master/YY.Admin.Core/Entity/MesXslUnit.cs create mode 100644 yy-admin-master/YY.Admin.Core/Util/CargoTareWeightCalculator.cs create mode 100644 yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyMatcher.cs create mode 100644 yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs create mode 100644 yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategySyncCoordinator.cs create mode 100644 yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogViewModel.cs create mode 100644 yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyListViewModel.cs create mode 100644 yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/TareStrategyPickerDialogViewModel.cs create mode 100644 yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml create mode 100644 yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml.cs create mode 100644 yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyListView.xaml create mode 100644 yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyListView.xaml.cs create mode 100644 yy-admin-master/YY.Admin/Views/RawMaterialEntry/TareStrategyPickerDialogView.xaml create mode 100644 yy-admin-master/YY.Admin/Views/RawMaterialEntry/TareStrategyPickerDialogView.xaml.cs create mode 100644 yy-admin-master/scripts/MenuFixTemp/MenuFixTemp.csproj create mode 100644 yy-admin-master/scripts/MenuFixTemp/Program.cs create mode 100644 yy-admin-master/scripts/ensure_tare_strategy_menu.ps1 diff --git a/XSLPrintDot/backend_windows.go b/XSLPrintDot/backend_windows.go index df39c97..efd21d6 100644 --- a/XSLPrintDot/backend_windows.go +++ b/XSLPrintDot/backend_windows.go @@ -324,17 +324,26 @@ func waitForWindowsPrintCompletion(printerName string, existingIDs map[int]bool, queued := false jobID := 0 + sumatraDone := false for { select { case err := <-cmdDone: + sumatraDone = true if err != nil && !queued { return fmt.Errorf("sumatra print failed: %v", err) } + // Sumatra 已正常退出且 spooler 未出现新任务:部分驱动/打印机直接出纸,不经过队列 + if err == nil && !queued { + return nil + } default: } now := time.Now() + if !queued && sumatraDone { + return nil + } if !queued && now.After(appearDeadline) { return fmt.Errorf("print job not queued within %s", printQueueAppearTimeout) } diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java index ace9fb2..126f6a7 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java @@ -211,6 +211,10 @@ public class ShiroConfig { filterChainDefinitionMap.put("/xslmes/mesXslWarehouse/anon/**", "anon"); // MES库区管理免密接口(供桌面端调用) filterChainDefinitionMap.put("/xslmes/mesXslWarehouseArea/anon/**", "anon"); + // MES密炼物料皮重策略免密接口(供桌面端调用) + filterChainDefinitionMap.put("/xslmes/mesXslMixerMaterialTareStrategy/anon/**", "anon"); + // MES单位只读免密接口(供桌面端单位下拉调用) + filterChainDefinitionMap.put("/xslmes/mesXslUnit/anon/**", "anon"); // MES密炼物料管理免密接口(供桌面端调用) filterChainDefinitionMap.put("/mes/material/mixerMaterial/anon/**", "anon"); // 打印模板免密接口(供桌面端调用) diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 index bd75771..e071428 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/doc/代码修改日志 @@ -521,3 +521,68 @@ jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules -- author:GHT---date:20260529--for: 【QH-MES审批流设计】审批IM消息升级为可跳转业务卡片(biz_record):点击可定位到对应单据,无法定位功能页时退回纯文本 --- jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java + +-- author:cursor---date:20250602--for: 【密炼物料皮重策略】桌面端同步 --- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerMaterialTareStrategyController.java +jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java + +-- author:cursor---date:20250602--for: 【密炼物料皮重策略】新增物料规格/托盘重量,皮重改名为包装物重量 --- +jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_118__mes_xsl_mixer_material_tare_strategy_fields.sql +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java +yy-admin-master/YY.Admin.Core/Entity/MesXslMixerMaterialTareStrategy.cs +yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs +yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyListView.xaml +yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml +yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogViewModel.cs +yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyListViewModel.cs +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue + +-- author:cursor---date:20250602--for: 【原料入场/原材料卡片】皮重字段落库 --- +jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_119__mes_xsl_raw_material_entry_tare_fields.sql +jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_120__mes_xsl_raw_material_card_tare_fields.sql +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java +yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs +yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs +yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs +yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs +yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs +yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs +yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardEditDialogViewModel.cs + +-- author:cursor---date:20250602--for: 【原料入场/原材料卡片】列表展示皮重相关字段 --- +yy-admin-master/YY.Admin/Views/RawMaterialEntry/RawMaterialEntryListView.xaml +yy-admin-master/YY.Admin/Views/RawMaterialCard/RawMaterialCardListView.xaml +jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts + +-- author:cursor---date:20250602--for: 【原料入场/原材料卡片】后端列表与详情展示皮重字段 --- +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialWorkshopRemain.java +jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialWorkshopRemain/MesXslRawMaterialWorkshopRemain.data.ts + +-- author:cursor---date:20250602--for: 【磅单记录】列表展示货物皮重(关联入场记录托盘及皮重合计,不落库) --- +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialEntryService.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialEntryServiceImpl.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +jeecgboot-vue3/src/views/xslmes/mesXslWeightRecord/MesXslWeightRecord.data.ts +yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs +yy-admin-master/YY.Admin.Core/Util/CargoTareWeightCalculator.cs +yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs +yy-admin-master/YY.Admin/Views/WeightRecord/WeightRecordListView.xaml +yy-admin-master/YY.Admin/Views/RawMaterialEntry/WeightRecordPickerDialogView.xaml + +-- author:cursor---date:20250602--for: 【磅单记录】列表展示原料重量(净重-货物皮重,不落库) --- +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java +jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialEntryServiceImpl.java +jeecgboot-vue3/src/views/xslmes/mesXslWeightRecord/MesXslWeightRecord.data.ts +yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs +yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs +yy-admin-master/YY.Admin/Views/WeightRecord/WeightRecordListView.xaml diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java index fa458a8..538091f 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslDesktopAnonController.java @@ -27,6 +27,8 @@ import org.jeecg.modules.xslmes.entity.MesXslCustomer; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry; import org.jeecg.modules.xslmes.entity.MesXslSupplier; +import org.jeecg.modules.xslmes.entity.MesXslMixerMaterialTareStrategy; +import org.jeecg.modules.xslmes.entity.MesXslUnit; import org.jeecg.modules.xslmes.entity.MesXslVehicle; import org.jeecg.modules.xslmes.entity.MesXslWarehouse; import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea; @@ -35,6 +37,8 @@ import org.jeecg.modules.xslmes.service.IMesXslCustomerService; import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService; import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService; import org.jeecg.modules.xslmes.service.IMesXslSupplierService; +import org.jeecg.modules.xslmes.service.IMesXslMixerMaterialTareStrategyService; +import org.jeecg.modules.xslmes.service.IMesXslUnitService; import org.jeecg.modules.xslmes.service.IMesXslVehicleService; import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService; import org.jeecg.modules.xslmes.service.IMesXslWarehouseService; @@ -60,6 +64,8 @@ import org.apache.commons.lang3.StringUtils; * ShiroConfig 白名单: * /xslmes/mesXslVehicle/anon/** * /xslmes/mesXslCustomer/anon/** + * /xslmes/mesXslMixerMaterialTareStrategy/anon/** + * /xslmes/mesXslUnit/anon/** */ @Tag(name = "桌面端免密接口") @RestController @@ -75,6 +81,8 @@ public class MesXslDesktopAnonController { private final IMesXslRawMaterialCardService rawMaterialCardService; private final IMesXslWarehouseService warehouseService; private final IMesXslWarehouseAreaService warehouseAreaService; + private final IMesXslMixerMaterialTareStrategyService tareStrategyService; + private final IMesXslUnitService unitService; private final MesXslStompNotifyService stompNotify; private final IPrintBizTemplateBindService printBizTemplateBindService; private final IPrintTemplateService printTemplateService; @@ -390,6 +398,7 @@ public class MesXslDesktopAnonController { QueryWrapper qw = QueryGenerator.initQueryWrapper(mesXslWeightRecord, req.getParameterMap()); qw.orderByDesc("create_time"); IPage page = weightRecordService.page(new Page<>(pageNo, pageSize), qw); + rawMaterialEntryService.fillWeightRecordDerivedFields(page.getRecords()); return Result.OK(page); } @@ -397,7 +406,11 @@ public class MesXslDesktopAnonController { @GetMapping("/xslmes/mesXslWeightRecord/anon/queryById") public Result weightRecordAnonQueryById(@RequestParam(name = "id") String id) { MesXslWeightRecord entity = weightRecordService.getById(id); - return entity != null ? Result.OK(entity) : Result.error("未找到对应数据"); + if (entity == null) { + return Result.error("未找到对应数据"); + } + rawMaterialEntryService.fillWeightRecordDerivedFields(Collections.singletonList(entity)); + return Result.OK(entity); } @Operation(summary = "磅单-免密添加") @@ -828,6 +841,90 @@ public class MesXslDesktopAnonController { return Result.OK("该值可用!"); } + // ═══════════════════════════ 密炼物料皮重策略 ═══════════════════════════ + + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端免密CRUD----------- + @Operation(summary = "密炼物料皮重策略-免密分页列表查询") + @GetMapping("/xslmes/mesXslMixerMaterialTareStrategy/anon/list") + public Result> tareStrategyAnonList( + MesXslMixerMaterialTareStrategy model, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap()); + qw.orderByDesc("effective_start_date", "create_time"); + IPage page = tareStrategyService.page(new Page<>(pageNo, pageSize), qw); + return Result.OK(page); + } + + @Operation(summary = "密炼物料皮重策略-免密通过id查询") + @GetMapping("/xslmes/mesXslMixerMaterialTareStrategy/anon/queryById") + public Result tareStrategyAnonQueryById(@RequestParam(name = "id") String id) { + MesXslMixerMaterialTareStrategy entity = tareStrategyService.getById(id); + return entity != null ? Result.OK(entity) : Result.error("未找到对应数据"); + } + + @Operation(summary = "密炼物料皮重策略-免密添加") + @PostMapping("/xslmes/mesXslMixerMaterialTareStrategy/anon/add") + public Result tareStrategyAnonAdd(@RequestBody MesXslMixerMaterialTareStrategy model) { + String err = tareStrategyService.validateBeforeSave(model, false); + if (err != null) { + return Result.error(err); + } + tareStrategyService.save(model); + stompNotify.publishMixerMaterialTareStrategyChanged("add", model.getId()); + return Result.OK("添加成功!"); + } + + @Operation(summary = "密炼物料皮重策略-免密编辑") + @RequestMapping(value = "/xslmes/mesXslMixerMaterialTareStrategy/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result tareStrategyAnonEdit(@RequestBody MesXslMixerMaterialTareStrategy model) { + if (oConvertUtils.isEmpty(model.getId())) { + return Result.error("主键不能为空"); + } + String err = tareStrategyService.validateBeforeSave(model, true); + if (err != null) { + return Result.error(err); + } + boolean ok = tareStrategyService.updateById(model); + if (!ok) { + return Result.error("数据已被他人修改,请刷新后重试"); + } + stompNotify.publishMixerMaterialTareStrategyChanged("edit", model.getId()); + return Result.OK("编辑成功!"); + } + + @Operation(summary = "密炼物料皮重策略-免密删除") + @DeleteMapping("/xslmes/mesXslMixerMaterialTareStrategy/anon/delete") + public Result tareStrategyAnonDelete(@RequestParam(name = "id") String id) { + tareStrategyService.removeById(id); + stompNotify.publishMixerMaterialTareStrategyChanged("delete", id); + return Result.OK("删除成功!"); + } + + @Operation(summary = "密炼物料皮重策略-免密批量删除") + @DeleteMapping("/xslmes/mesXslMixerMaterialTareStrategy/anon/deleteBatch") + public Result tareStrategyAnonDeleteBatch(@RequestParam(name = "ids") String ids) { + tareStrategyService.removeByIds(Arrays.asList(ids.split(","))); + stompNotify.publishMixerMaterialTareStrategyChanged("batchDelete", ids); + return Result.OK("批量删除成功!"); + } + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端免密CRUD----------- + + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端单位下拉只读----------- + @Operation(summary = "单位-免密分页列表查询(供桌面端单位下拉)") + @GetMapping("/xslmes/mesXslUnit/anon/list") + public Result> unitAnonList( + MesXslUnit mesXslUnit, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "1000") Integer pageSize, + HttpServletRequest req) { + QueryWrapper qw = QueryGenerator.initQueryWrapper(mesXslUnit, req.getParameterMap()); + IPage page = unitService.page(new Page<>(pageNo, pageSize), qw); + return Result.OK(page); + } + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端单位下拉只读----------- + // ─────────────────────────── 车辆私有辅助 ──────────────────────────── private void applyWeightNetAndBillType(MesXslWeightRecord record) { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerMaterialTareStrategyController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerMaterialTareStrategyController.java new file mode 100644 index 0000000..06143cf --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerMaterialTareStrategyController.java @@ -0,0 +1,151 @@ +package org.jeecg.modules.xslmes.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.aspect.annotation.AutoLog; +import org.jeecg.common.system.base.controller.JeecgController; +import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.xslmes.entity.MesXslMixerMaterialTareStrategy; +import org.jeecg.modules.xslmes.service.IMesXslMixerMaterialTareStrategyService; +import org.jeecg.modules.xslmes.service.MesXslStompNotifyService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +/** + * MES 密炼物料皮重策略 + */ +@Tag(name = "MES密炼物料皮重策略") +@RestController +@RequestMapping("/xslmes/mesXslMixerMaterialTareStrategy") +@Slf4j +public class MesXslMixerMaterialTareStrategyController + extends JeecgController { + + @Autowired + private IMesXslMixerMaterialTareStrategyService mesXslMixerMaterialTareStrategyService; + + @Autowired + private MesXslStompNotifyService stompNotify; + + @Operation(summary = "MES密炼物料皮重策略-分页列表查询") + @GetMapping(value = "/list") + public Result> queryPageList( + MesXslMixerMaterialTareStrategy model, + @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + HttpServletRequest req) { + QueryWrapper queryWrapper = + QueryGenerator.initQueryWrapper(model, req.getParameterMap()); + queryWrapper.orderByDesc("effective_start_date", "create_time"); + Page page = new Page<>(pageNo, pageSize); + IPage pageList = mesXslMixerMaterialTareStrategyService.page(page, queryWrapper); + return Result.OK(pageList); + } + + @AutoLog(value = "MES密炼物料皮重策略-添加") + @Operation(summary = "MES密炼物料皮重策略-添加") + @RequiresPermissions("xslmes:mes_xsl_mixer_material_tare_strategy:add") + @PostMapping(value = "/add") + public Result add(@RequestBody MesXslMixerMaterialTareStrategy model) { + fillMaintainBy(model); + String err = mesXslMixerMaterialTareStrategyService.validateBeforeSave(model, false); + if (err != null) { + return Result.error(err); + } + mesXslMixerMaterialTareStrategyService.save(model); + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + stompNotify.publishMixerMaterialTareStrategyChanged("add", model.getId()); + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + return Result.OK("添加成功!"); + } + + @AutoLog(value = "MES密炼物料皮重策略-编辑") + @Operation(summary = "MES密炼物料皮重策略-编辑") + @RequiresPermissions("xslmes:mes_xsl_mixer_material_tare_strategy:edit") + @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) + public Result edit(@RequestBody MesXslMixerMaterialTareStrategy model) { + fillMaintainBy(model); + String err = mesXslMixerMaterialTareStrategyService.validateBeforeSave(model, true); + if (err != null) { + return Result.error(err); + } + mesXslMixerMaterialTareStrategyService.updateById(model); + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + stompNotify.publishMixerMaterialTareStrategyChanged("edit", model.getId()); + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + return Result.OK("编辑成功!"); + } + + @AutoLog(value = "MES密炼物料皮重策略-删除") + @Operation(summary = "MES密炼物料皮重策略-通过id删除") + @RequiresPermissions("xslmes:mes_xsl_mixer_material_tare_strategy:delete") + @DeleteMapping(value = "/delete") + public Result delete(@RequestParam(name = "id", required = true) String id) { + mesXslMixerMaterialTareStrategyService.removeById(id); + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + stompNotify.publishMixerMaterialTareStrategyChanged("delete", id); + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + return Result.OK("删除成功!"); + } + + @AutoLog(value = "MES密炼物料皮重策略-批量删除") + @Operation(summary = "MES密炼物料皮重策略-批量删除") + @RequiresPermissions("xslmes:mes_xsl_mixer_material_tare_strategy:deleteBatch") + @DeleteMapping(value = "/deleteBatch") + public Result deleteBatch(@RequestParam(name = "ids", required = true) String ids) { + mesXslMixerMaterialTareStrategyService.removeByIds(Arrays.asList(ids.split(","))); + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + stompNotify.publishMixerMaterialTareStrategyChanged("batchDelete", ids); + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + return Result.OK("批量删除成功!"); + } + + @Operation(summary = "MES密炼物料皮重策略-通过id查询") + @GetMapping(value = "/queryById") + public Result queryById(@RequestParam(name = "id", required = true) String id) { + MesXslMixerMaterialTareStrategy entity = mesXslMixerMaterialTareStrategyService.getById(id); + if (entity == null) { + return Result.error("未找到对应数据"); + } + return Result.OK(entity); + } + + @RequiresPermissions("xslmes:mes_xsl_mixer_material_tare_strategy:exportXls") + @RequestMapping(value = "/exportXls") + public ModelAndView exportXls(HttpServletRequest request, MesXslMixerMaterialTareStrategy model) { + return super.exportXls(request, model, MesXslMixerMaterialTareStrategy.class, "密炼物料皮重策略"); + } + + @RequiresPermissions("xslmes:mes_xsl_mixer_material_tare_strategy:importExcel") + @RequestMapping(value = "/importExcel", method = RequestMethod.POST) + public Result importExcel(HttpServletRequest request, HttpServletResponse response) { + return super.importExcel(request, response, MesXslMixerMaterialTareStrategy.class); + } + + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】维护人自动回填当前登录用户----------- + private void fillMaintainBy(MesXslMixerMaterialTareStrategy model) { + LoginUser loginUser = null; + try { + loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + } catch (Exception e) { + log.debug("获取登录用户失败", e); + } + if (loginUser != null && oConvertUtils.isNotEmpty(loginUser.getUsername())) { + model.setMaintainBy(loginUser.getUsername()); + } + } + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】维护人自动回填当前登录用户----------- +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java index 08cfd8d..c99264c 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslWeightRecordController.java @@ -27,9 +27,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.Map; import java.util.Random; -import java.util.stream.Collectors; /** * 地磅数据记录 @@ -57,7 +55,7 @@ public class MesXslWeightRecordController extends JeecgController page = new Page<>(pageNo, pageSize); IPage pageList = mesXslWeightRecordService.page(page, queryWrapper); - fillEnteredWeight(pageList.getRecords()); + rawMaterialEntryService.fillWeightRecordDerivedFields(pageList.getRecords()); return Result.OK(pageList); } @@ -120,7 +118,7 @@ public class MesXslWeightRecordController extends JeecgController records) { - if (records == null || records.isEmpty()) { - return; - } - List billNos = records.stream() - .map(MesXslWeightRecord::getBillNo) - .filter(s -> s != null && !s.isBlank()) - .distinct() - .collect(Collectors.toList()); - if (billNos.isEmpty()) { - return; - } - Map sumMap = rawMaterialEntryService.sumEnteredWeightByBillNos(billNos); - for (MesXslWeightRecord r : records) { - BigDecimal v = (r.getBillNo() == null) ? null : sumMap.get(r.getBillNo()); - r.setEnteredWeight(v != null ? v : BigDecimal.ZERO); - } + private void fillWeightRecordDerivedFields(List records) { + rawMaterialEntryService.fillWeightRecordDerivedFields(records); } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java new file mode 100644 index 0000000..e76bc29 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java @@ -0,0 +1,107 @@ +package org.jeecg.modules.xslmes.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.jeecg.common.aspect.annotation.Dict; +import org.jeecgframework.poi.excel.annotation.Excel; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * MES 密炼物料皮重策略 + */ +@Data +@TableName("mes_xsl_mixer_material_tare_strategy") +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = false) +@Schema(description = "MES密炼物料皮重策略") +public class MesXslMixerMaterialTareStrategy implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + private String id; + + @Schema(description = "租户ID") + private Integer tenantId; + + @Excel(name = "密炼物料", width = 18, dictTable = "mes_mixer_material", dicText = "material_name", dicCode = "id") + @Dict(dictTable = "mes_mixer_material", dicText = "material_name", dicCode = "id") + @Schema(description = "密炼物料ID(关联 mes_mixer_material.id)") + private String mixerMaterialId; + + @Excel(name = "密炼物料名称", width = 20) + @Schema(description = "密炼物料名称冗余") + private String mixerMaterialName; + + @Excel(name = "供应商", width = 18, dictTable = "mes_xsl_supplier", dicText = "supplier_name", dicCode = "id") + @Dict(dictTable = "mes_xsl_supplier", dicText = "supplier_name", dicCode = "id") + @Schema(description = "供应商ID(关联 mes_xsl_supplier.id)") + private String supplierId; + + @Excel(name = "供应商名称", width = 20) + @Schema(description = "供应商名称冗余") + private String supplierName; + + @Excel(name = "物料规格", width = 16) + @Schema(description = "物料规格(与密炼物料、供应商、生效日期共同参与唯一性校验,不同规格可分别维护)") + private String materialSpec; + + @Excel(name = "包装物重量", width = 12) + @Schema(description = "包装物重量") + private BigDecimal tareWeight; + + @Excel(name = "托盘重量", width = 12) + @Schema(description = "托盘重量") + private BigDecimal palletWeight; + + @Schema(description = "单位ID(关联 mes_xsl_unit.id)") + private String unitId; + + @Excel(name = "单位", width = 10) + @Schema(description = "单位名称冗余") + private String unitName; + + @Excel(name = "生效开始日期", width = 14, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Schema(description = "生效开始日期") + private Date effectiveStartDate; + + @Excel(name = "生效截止日期", width = 14, format = "yyyy-MM-dd") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + @Schema(description = "生效截止日期") + private Date effectiveEndDate; + + @Excel(name = "维护人", width = 12, dictTable = "sys_user", dicText = "realname", dicCode = "username") + @Dict(dictTable = "sys_user", dicText = "realname", dicCode = "username") + @Schema(description = "维护人(登录账号)") + private String maintainBy; + + @Excel(name = "创建人", width = 12) + private String createBy; + + @Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @Excel(name = "修改人", width = 12) + private String updateBy; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + + private String sysOrgCode; + private Integer delFlag; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java index 96ccbd4..7400e7a 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialCard.java @@ -86,6 +86,14 @@ public class MesXslRawMaterialCard implements Serializable { @Schema(description = "总重") private BigDecimal totalWeight; + @Excel(name = "包装物皮重", width = 12) + @Schema(description = "包装物皮重(KG)") + private BigDecimal packagingTare; + + @Excel(name = "托盘重量", width = 12) + @Schema(description = "托盘重量(KG)") + private BigDecimal palletWeight; + @Excel(name = "剩余重量", width = 12) @Schema(description = "剩余重量") private BigDecimal remainingWeight; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java index 9635bcf..21db0a6 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialEntry.java @@ -89,6 +89,10 @@ public class MesXslRawMaterialEntry implements Serializable { @Schema(description = "总重(KG)") private BigDecimal totalWeight; + @Excel(name = "托盘及皮重合计", width = 14) + @Schema(description = "托盘及皮重(合计)") + private BigDecimal palletTareTotal; + // 总份数 / 每份总重 / 每份包数:从 数值类型 升级为 字符串类型, // 支持桌面端「拆码明细」多行拼接保存(如 20/1/ 与 100/200/)。 @Excel(name = "总份数", width = 12) @@ -99,6 +103,17 @@ public class MesXslRawMaterialEntry implements Serializable { @Schema(description = "每份总重(KG)(支持多行拆码明细拼接,如 100/200/)") private String portionWeight; + @Excel(name = "包装物皮重", width = 14) + @Schema(description = "拆码明细包装物皮重拼接(以 / 分隔,末尾带 /)") + private String portionPackagingTare; + + @Excel(name = "托盘重量", width = 14) + @Schema(description = "拆码明细托盘重量拼接(以 / 分隔,末尾带 /)") + private String portionPalletWeight; + + @Schema(description = "拆码明细皮重策略ID拼接(以 / 分隔,末尾带 /)") + private String portionTareStrategyIds; + @Excel(name = "每份包数", width = 12) @Schema(description = "每份包数(支持多行拆码明细拼接)") private String portionPackages; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialWorkshopRemain.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialWorkshopRemain.java index 6c5ea9b..c5555eb 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialWorkshopRemain.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslRawMaterialWorkshopRemain.java @@ -56,6 +56,14 @@ public class MesXslRawMaterialWorkshopRemain extends JeecgEntity { @Schema(description = "总重") private BigDecimal totalWeight; + @Excel(name = "包装物皮重", width = 12) + @Schema(description = "包装物皮重(KG)") + private BigDecimal packagingTare; + + @Excel(name = "托盘重量", width = 12) + @Schema(description = "托盘重量(KG)") + private BigDecimal palletWeight; + @Excel(name = "剩余重量", width = 12) @Schema(description = "剩余重量") private BigDecimal remainingWeight; diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java index 26f8753..dd64966 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslWeightRecord.java @@ -43,7 +43,10 @@ import java.util.Date; "driverName", "driverPhone", "billType", - "tenantId" + "tenantId", + "enteredWeight", + "cargoTareWeight", + "rawMaterialWeight" }) public class MesXslWeightRecord extends JeecgEntity implements Serializable { @@ -129,4 +132,20 @@ public class MesXslWeightRecord extends JeecgEntity implements Serializable { @TableField(exist = false) @Schema(description = "已入场重量(KG),由原料入场记录的拆码明细实时累计") private BigDecimal enteredWeight; + + /** + * 货物皮重(KG)—— 实时计算,不落库。 + * 数据来源:所有引用本榜单(bill_no 匹配)的原料入场记录的 pallet_tare_total(托盘及皮重合计)累加。 + */ + @TableField(exist = false) + @Schema(description = "货物皮重(KG),关联原料入场记录的托盘及皮重合计累计") + private BigDecimal cargoTareWeight; + + /** + * 原料重量(KG)—— 实时计算,不落库。 + * 公式:净重(KG) - 货物皮重(KG)。 + */ + @TableField(exist = false) + @Schema(description = "原料重量(KG)=净重-货物皮重") + private BigDecimal rawMaterialWeight; } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixerMaterialTareStrategyMapper.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixerMaterialTareStrategyMapper.java new file mode 100644 index 0000000..1416313 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixerMaterialTareStrategyMapper.java @@ -0,0 +1,11 @@ +package org.jeecg.modules.xslmes.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.jeecg.modules.xslmes.entity.MesXslMixerMaterialTareStrategy; + +/** + * MES 密炼物料皮重策略 Mapper + */ +@Mapper +public interface MesXslMixerMaterialTareStrategyMapper extends BaseMapper {} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerMaterialTareStrategyService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerMaterialTareStrategyService.java new file mode 100644 index 0000000..6328bbb --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerMaterialTareStrategyService.java @@ -0,0 +1,22 @@ +package org.jeecg.modules.xslmes.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.jeecg.modules.xslmes.entity.MesXslMixerMaterialTareStrategy; + +/** + * MES 密炼物料皮重策略 + */ +public interface IMesXslMixerMaterialTareStrategyService extends IService { + + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】保存前校验生效日期重叠(含物料规格)----------- + /** + * 校验同一租户、同一供应商、同一密炼物料、同一物料规格在生效日期内是否已存在记录。 + * 同一密炼物料不同规格可分别维护;仅规格相同且生效日期重叠时不允许重复。 + * + * @param entity 待保存实体 + * @param isUpdate 是否编辑 + * @return 错误信息,null 表示通过 + */ + String validateBeforeSave(MesXslMixerMaterialTareStrategy entity, boolean isUpdate); + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】保存前校验生效日期重叠(含物料规格)----------- +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialEntryService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialEntryService.java index a38537a..3f45bbc 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialEntryService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialEntryService.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry; +import org.jeecg.modules.xslmes.entity.MesXslWeightRecord; import org.jeecg.modules.xslmes.vo.MesXslRawMaterialCardBriefVO; import org.jeecg.modules.xslmes.vo.MesXslRawMaterialEntryDeleteLogVO; @@ -62,6 +63,19 @@ public interface IMesXslRawMaterialEntryService extends IService sumEnteredWeightByBillNos(Collection billNos); + /** + * 按榜单号批量统计「货物皮重」(托盘及皮重合计累加)。 + * + * @param billNos 榜单号集合 + * @return billNo -> 累计货物皮重;查不到的 billNo 不会出现在 map 中 + */ + Map sumCargoTareByBillNos(Collection billNos); + + /** + * 给磅单列表/详情填充由原料入场记录衍生的 transient 字段(已入场重量、货物皮重)。 + */ + void fillWeightRecordDerivedFields(List records); + /** * 结存入库并汇总原材料库存。 *

diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java index d55282c..72308ef 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/MesXslStompNotifyService.java @@ -75,6 +75,14 @@ public class MesXslStompNotifyService { publish("/topic/sync/print-templates", "PRINT_TEMPLATE_CHANGED", "templateId", templateId, action); } + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + /** 广播密炼物料皮重策略变更事件到 /topic/sync/mes-mixer-material-tare-strategies */ + public void publishMixerMaterialTareStrategyChanged(String action, String tareStrategyId) { + publish("/topic/sync/mes-mixer-material-tare-strategies", "MES_MIXER_MATERIAL_TARE_STRATEGY_CHANGED", + "tareStrategyId", tareStrategyId, action); + } + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】桌面端同步----------- + // ─────────────────────────── 私有辅助 ──────────────────────────── private void publish(String topic, String cmd, String idKey, String idValue, String action) { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java new file mode 100644 index 0000000..84ce093 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java @@ -0,0 +1,107 @@ +package org.jeecg.modules.xslmes.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import java.util.Date; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.mes.material.entity.MesMixerMaterial; +import org.jeecg.modules.mes.material.service.IMesMixerMaterialService; +import org.jeecg.modules.xslmes.entity.MesXslMixerMaterialTareStrategy; +import org.jeecg.modules.xslmes.entity.MesXslSupplier; +import org.jeecg.modules.xslmes.entity.MesXslUnit; +import org.jeecg.modules.xslmes.mapper.MesXslMixerMaterialTareStrategyMapper; +import org.jeecg.modules.xslmes.service.IMesXslMixerMaterialTareStrategyService; +import org.jeecg.modules.xslmes.service.IMesXslSupplierService; +import org.jeecg.modules.xslmes.service.IMesXslUnitService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * MES 密炼物料皮重策略 + */ +@Service +public class MesXslMixerMaterialTareStrategyServiceImpl + extends ServiceImpl + implements IMesXslMixerMaterialTareStrategyService { + + @Autowired + private IMesMixerMaterialService mesMixerMaterialService; + + @Autowired + private IMesXslSupplierService mesXslSupplierService; + + @Autowired + private IMesXslUnitService mesXslUnitService; + + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】保存前校验与冗余回填----------- + @Override + public String validateBeforeSave(MesXslMixerMaterialTareStrategy entity, boolean isUpdate) { + if (oConvertUtils.isEmpty(entity.getMixerMaterialId())) { + return "请选择密炼物料"; + } + if (oConvertUtils.isEmpty(entity.getSupplierId())) { + return "请选择供应商"; + } + if (entity.getTareWeight() == null) { + return "请填写包装物重量"; + } + if (entity.getPalletWeight() != null && entity.getPalletWeight().signum() < 0) { + return "托盘重量不能为负数"; + } + if (oConvertUtils.isEmpty(entity.getUnitId())) { + return "请选择单位"; + } + Date startDate = entity.getEffectiveStartDate(); + Date endDate = entity.getEffectiveEndDate(); + if (startDate == null || endDate == null) { + return "请填写完整的生效日期"; + } + if (startDate.after(endDate)) { + return "生效开始日期不能晚于截止日期"; + } + + MesMixerMaterial mixerMaterial = mesMixerMaterialService.getById(entity.getMixerMaterialId()); + if (mixerMaterial == null) { + return "所选密炼物料不存在,请重新选择"; + } + MesXslSupplier supplier = mesXslSupplierService.getById(entity.getSupplierId()); + if (supplier == null) { + return "所选供应商不存在,请重新选择"; + } + MesXslUnit unit = mesXslUnitService.getById(entity.getUnitId()); + if (unit == null) { + return "所选单位不存在,请重新选择"; + } + + entity.setMixerMaterialName(mixerMaterial.getMaterialName()); + entity.setSupplierName(supplier.getSupplierName()); + entity.setUnitName(unit.getUnitName()); + //update-begin---author:cursor ---date:20250602 for:【密炼物料皮重策略】重叠校验增加物料规格维度----------- + if (entity.getMaterialSpec() != null) { + entity.setMaterialSpec(entity.getMaterialSpec().trim()); + } + if (oConvertUtils.isEmpty(entity.getMaterialSpec())) { + entity.setMaterialSpec(null); + } + + String normalizedSpec = oConvertUtils.isEmpty(entity.getMaterialSpec()) ? "" : entity.getMaterialSpec(); + LambdaQueryWrapper overlapQw = new LambdaQueryWrapper<>(); + overlapQw.eq(MesXslMixerMaterialTareStrategy::getMixerMaterialId, entity.getMixerMaterialId()) + .eq(MesXslMixerMaterialTareStrategy::getSupplierId, entity.getSupplierId()) + .le(MesXslMixerMaterialTareStrategy::getEffectiveStartDate, endDate) + .ge(MesXslMixerMaterialTareStrategy::getEffectiveEndDate, startDate) + .apply("IFNULL(TRIM(material_spec), '') = {0}", normalizedSpec); + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】重叠校验增加物料规格维度----------- + if (entity.getTenantId() != null) { + overlapQw.eq(MesXslMixerMaterialTareStrategy::getTenantId, entity.getTenantId()); + } + if (isUpdate && oConvertUtils.isNotEmpty(entity.getId())) { + overlapQw.ne(MesXslMixerMaterialTareStrategy::getId, entity.getId()); + } + if (count(overlapQw) > 0) { + return "同一租户、同一供应商、同一密炼物料且物料规格相同的时间段内,已存在策略,请勿重复维护"; + } + return null; + } + //update-end---author:cursor ---date:20250602 for:【密炼物料皮重策略】保存前校验与冗余回填----------- +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialEntryServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialEntryServiceImpl.java index 9d53511..57dac60 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialEntryServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRawMaterialEntryServiceImpl.java @@ -10,6 +10,7 @@ import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialInventory; import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry; import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea; +import org.jeecg.modules.xslmes.entity.MesXslWeightRecord; import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialEntryMapper; import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService; import org.jeecg.modules.xslmes.service.IMesXslRawMaterialInventoryService; @@ -217,6 +218,75 @@ public class MesXslRawMaterialEntryServiceImpl return result; } + @Override + public Map sumCargoTareByBillNos(Collection billNos) { + if (billNos == null || billNos.isEmpty()) { + return Collections.emptyMap(); + } + Set distinct = billNos.stream() + .filter(s -> s != null && !s.isBlank()) + .collect(Collectors.toCollection(HashSet::new)); + if (distinct.isEmpty()) { + return Collections.emptyMap(); + } + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.in(MesXslRawMaterialEntry::getBillNo, distinct) + .select(MesXslRawMaterialEntry::getBillNo, MesXslRawMaterialEntry::getPalletTareTotal); + List rows = this.list(qw); + Map result = new HashMap<>(); + for (MesXslRawMaterialEntry row : rows) { + if (row.getBillNo() == null || row.getPalletTareTotal() == null) { + continue; + } + result.merge(row.getBillNo(), row.getPalletTareTotal(), BigDecimal::add); + } + return result; + } + + @Override + public void fillWeightRecordDerivedFields(List records) { + if (records == null || records.isEmpty()) { + return; + } + List billNos = records.stream() + .map(MesXslWeightRecord::getBillNo) + .filter(s -> s != null && !s.isBlank()) + .distinct() + .collect(Collectors.toList()); + if (billNos.isEmpty()) { + for (MesXslWeightRecord r : records) { + r.setEnteredWeight(BigDecimal.ZERO); + r.setCargoTareWeight(BigDecimal.ZERO); + applyRawMaterialWeight(r); + } + return; + } + Map enteredMap = sumEnteredWeightByBillNos(billNos); + Map cargoTareMap = sumCargoTareByBillNos(billNos); + for (MesXslWeightRecord r : records) { + String billNo = r.getBillNo(); + if (billNo == null || billNo.isBlank()) { + r.setEnteredWeight(BigDecimal.ZERO); + r.setCargoTareWeight(BigDecimal.ZERO); + } else { + r.setEnteredWeight(enteredMap.getOrDefault(billNo, BigDecimal.ZERO)); + r.setCargoTareWeight(cargoTareMap.getOrDefault(billNo, BigDecimal.ZERO)); + } + applyRawMaterialWeight(r); + } + } + + /** 原料重量 = 净重 - 货物皮重(不落库) */ + private static void applyRawMaterialWeight(MesXslWeightRecord record) { + BigDecimal net = record.getNetWeight(); + if (net == null) { + record.setRawMaterialWeight(null); + return; + } + BigDecimal cargo = record.getCargoTareWeight() != null ? record.getCargoTareWeight() : BigDecimal.ZERO; + record.setRawMaterialWeight(net.subtract(cargo)); + } + @Override @Transactional(rollbackFor = Exception.class) public void batchStockInAndSyncInventory(Collection ids) { diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 b/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 index 01b0b7c..b57e140 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/docs/代码修改日志 @@ -540,3 +540,34 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTes jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue + +-- author:cursor---date:20250602--for: 【密炼物料皮重策略】建表、CRUD、生效日期重叠校验、菜单授权 --- +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_mixer_material_tare_strategy.sql +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mapper/MesXslMixerMaterialTareStrategyMapper.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerMaterialTareStrategyService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerMaterialTareStrategyController.java +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategyList.vue +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.api.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue + +-- author:cursor---date:20250602--for: 【密炼物料皮重策略】新增物料规格/托盘重量,皮重改名为包装物重量 --- +jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_118__mes_xsl_mixer_material_tare_strategy_fields.sql +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java +yy-admin-master/YY.Admin.Core/Entity/MesXslMixerMaterialTareStrategy.cs +yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs +yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyListView.xaml +yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml +yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogViewModel.cs +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue + +-- author:cursor---date:20250602--for: 【密炼物料皮重策略】重叠校验明确纳入物料规格维度 --- +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerMaterialTareStrategyService.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerMaterialTareStrategyServiceImpl.java +jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/entity/MesXslMixerMaterialTareStrategy.java +jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts +yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_mixer_material_tare_strategy.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_mixer_material_tare_strategy.sql new file mode 100644 index 0000000..7ea7802 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_mixer_material_tare_strategy.sql @@ -0,0 +1,121 @@ +-- 密炼物料皮重策略:建表 + 菜单(挂 MES基础资料)+ admin 授权 +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `mes_xsl_mixer_material_tare_strategy` ( + `id` varchar(32) NOT NULL COMMENT '主键', + `tenant_id` int DEFAULT NULL COMMENT '租户ID', + `mixer_material_id` varchar(32) NOT NULL COMMENT '密炼物料ID(关联 mes_mixer_material.id)', + `mixer_material_name` varchar(200) DEFAULT NULL COMMENT '密炼物料名称冗余', + `supplier_id` varchar(36) NOT NULL COMMENT '供应商ID(关联 mes_xsl_supplier.id)', + `supplier_name` varchar(100) DEFAULT NULL COMMENT '供应商名称冗余', + `tare_weight` decimal(12,3) NOT NULL COMMENT '皮重', + `unit_id` varchar(36) DEFAULT NULL COMMENT '单位ID(关联 mes_xsl_unit.id)', + `unit_name` varchar(64) DEFAULT NULL COMMENT '单位名称冗余', + `effective_start_date` date NOT NULL COMMENT '生效开始日期', + `effective_end_date` date NOT NULL COMMENT '生效截止日期', + `maintain_by` varchar(50) DEFAULT NULL COMMENT '维护人(登录账号)', + `sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门', + `create_by` varchar(50) DEFAULT NULL COMMENT '创建人', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(50) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `del_flag` int NOT NULL DEFAULT 0 COMMENT '逻辑删除(0正常 1已删除)', + PRIMARY KEY (`id`), + KEY `idx_mxmts_tenant_supplier_material` (`tenant_id`, `supplier_id`, `mixer_material_id`), + KEY `idx_mxmts_effective_dates` (`effective_start_date`, `effective_end_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES密炼物料皮重策略'; + +SET @mes_tenant_id = 1002; + +SET @mes_base_pid = ( + SELECT MIN(`id`) FROM `sys_permission` + WHERE `del_flag` = 0 AND `menu_type` = 0 AND `name` IN ('MES基础资料', 'MES资料') +); +SET @mes_base_pid = IFNULL(@mes_base_pid, '1860000000000000001'); + +UPDATE `sys_permission` +SET `is_leaf` = 0, `update_time` = NOW() +WHERE `id` = @mes_base_pid AND `is_leaf` = 1; + +INSERT INTO `sys_permission` ( + `id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, + `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, + `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, + `del_flag`, `rule_flag`, `status`, `internal_or_external` +) +SELECT + '177925970995580', @mes_base_pid, '密炼物料皮重策略', '/xslmes/mesXslMixerMaterialTareStrategy', + 'xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategyList', 1, 'MesXslMixerMaterialTareStrategyList', NULL, + 1, NULL, '0', 18.00, 0, 'ant-design:database-outlined', 0, 1, + 0, 0, 'MES密炼物料皮重策略', 'admin', NOW(), 'admin', NOW(), + 0, 0, '1', 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_permission` + WHERE `id` = '177925970995580' + OR (`del_flag` = 0 AND `menu_type` = 1 AND `name` = '密炼物料皮重策略' AND `parent_id` = @mes_base_pid) +); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT '177925970995581', '177925970995580', '新增', 2, 'xslmes:mes_xsl_mixer_material_tare_strategy:add', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '177925970995581'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT '177925970995582', '177925970995580', '编辑', 2, 'xslmes:mes_xsl_mixer_material_tare_strategy:edit', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '177925970995582'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT '177925970995583', '177925970995580', '删除', 2, 'xslmes:mes_xsl_mixer_material_tare_strategy:delete', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '177925970995583'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT '177925970995584', '177925970995580', '批量删除', 2, 'xslmes:mes_xsl_mixer_material_tare_strategy:deleteBatch', '1', 4.00, 0, 1, 0, '1', 0, 'admin', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '177925970995584'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT '177925970995585', '177925970995580', '导出', 2, 'xslmes:mes_xsl_mixer_material_tare_strategy:exportXls', '1', 5.00, 0, 1, 0, '1', 0, 'admin', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '177925970995585'); + +INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`) +SELECT '177925970995586', '177925970995580', '导入', 2, 'xslmes:mes_xsl_mixer_material_tare_strategy:importExcel', '1', 6.00, 0, 1, 0, '1', 0, 'admin', NOW() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_permission` WHERE `id` = '177925970995586'); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1' +FROM `sys_role` r +CROSS JOIN `sys_permission` p +WHERE r.`tenant_id` = @mes_tenant_id + AND r.`role_code` = 'admin' + AND p.`id` IN ( + '177925970995580', + '177925970995581', + '177925970995582', + '177925970995583', + '177925970995584', + '177925970995585', + '177925970995586' + ) + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_permission` rp + WHERE rp.`role_id` = r.id AND rp.`permission_id` = p.id + ); + +INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`) +SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1' +FROM `sys_role` r +CROSS JOIN `sys_permission` p +WHERE r.`role_code` = 'admin' + AND r.`tenant_id` IS NULL + AND p.`id` IN ( + '177925970995580', + '177925970995581', + '177925970995582', + '177925970995583', + '177925970995584', + '177925970995585', + '177925970995586' + ) + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_permission` rp + WHERE rp.`role_id` = r.id AND rp.`permission_id` = p.id + ); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_118__mes_xsl_mixer_material_tare_strategy_fields.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_118__mes_xsl_mixer_material_tare_strategy_fields.sql new file mode 100644 index 0000000..5f8a544 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_118__mes_xsl_mixer_material_tare_strategy_fields.sql @@ -0,0 +1,9 @@ +-- 密炼物料皮重策略:新增物料规格、托盘重量;皮重字段注释改为包装物重量 +SET NAMES utf8mb4; + +ALTER TABLE `mes_xsl_mixer_material_tare_strategy` + ADD COLUMN `material_spec` varchar(200) DEFAULT NULL COMMENT '物料规格' AFTER `supplier_name`, + ADD COLUMN `pallet_weight` decimal(12,3) DEFAULT NULL COMMENT '托盘重量' AFTER `tare_weight`; + +ALTER TABLE `mes_xsl_mixer_material_tare_strategy` + MODIFY COLUMN `tare_weight` decimal(12,3) NOT NULL COMMENT '包装物重量'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_119__mes_xsl_raw_material_entry_tare_fields.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_119__mes_xsl_raw_material_entry_tare_fields.sql new file mode 100644 index 0000000..59018f4 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_119__mes_xsl_raw_material_entry_tare_fields.sql @@ -0,0 +1,8 @@ +-- 原料入场记录:托盘及皮重合计 + 拆码明细皮重策略相关字段 +SET NAMES utf8mb4; + +ALTER TABLE `mes_xsl_raw_material_entry` + ADD COLUMN `pallet_tare_total` decimal(12,3) DEFAULT NULL COMMENT '托盘及皮重(合计)' AFTER `total_weight`, + ADD COLUMN `portion_packaging_tare` varchar(500) DEFAULT NULL COMMENT '拆码明细包装物皮重拼接(以/分隔,末尾带/)' AFTER `portion_weight`, + ADD COLUMN `portion_pallet_weight` varchar(500) DEFAULT NULL COMMENT '拆码明细托盘重量拼接(以/分隔,末尾带/)' AFTER `portion_packaging_tare`, + ADD COLUMN `portion_tare_strategy_ids` varchar(1000) DEFAULT NULL COMMENT '拆码明细皮重策略ID拼接(以/分隔,末尾带/)' AFTER `portion_pallet_weight`; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_120__mes_xsl_raw_material_card_tare_fields.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_120__mes_xsl_raw_material_card_tare_fields.sql new file mode 100644 index 0000000..0d59fc1 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_120__mes_xsl_raw_material_card_tare_fields.sql @@ -0,0 +1,6 @@ +-- 原材料卡片:包装物皮重、托盘重量 +SET NAMES utf8mb4; + +ALTER TABLE `mes_xsl_raw_material_card` + ADD COLUMN `packaging_tare` decimal(12,3) DEFAULT NULL COMMENT '包装物皮重(KG)' AFTER `total_weight`, + ADD COLUMN `pallet_weight` decimal(12,3) DEFAULT NULL COMMENT '托盘重量(KG)' AFTER `packaging_tare`; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.api.ts b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.api.ts new file mode 100644 index 0000000..783c00f --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.api.ts @@ -0,0 +1,30 @@ +import { defHttp } from '/@/utils/http/axios'; + +enum Api { + list = '/xslmes/mesXslMixerMaterialTareStrategy/list', + save = '/xslmes/mesXslMixerMaterialTareStrategy/add', + edit = '/xslmes/mesXslMixerMaterialTareStrategy/edit', + deleteOne = '/xslmes/mesXslMixerMaterialTareStrategy/delete', + deleteBatch = '/xslmes/mesXslMixerMaterialTareStrategy/deleteBatch', + importExcel = '/xslmes/mesXslMixerMaterialTareStrategy/importExcel', + exportXls = '/xslmes/mesXslMixerMaterialTareStrategy/exportXls', + queryById = '/xslmes/mesXslMixerMaterialTareStrategy/queryById', +} + +export const list = (params) => defHttp.get({ url: Api.list, params }); + +export const queryById = (params) => defHttp.get({ url: Api.queryById, params }); + +export const deleteOne = (params, handleSuccess) => + defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess()); + +export const batchDelete = (params, handleSuccess) => + defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }).then(() => handleSuccess()); + +export const saveOrUpdate = (params, isUpdate) => { + const url = isUpdate ? Api.edit : Api.save; + return defHttp.post({ url, params }, { successMessageMode: 'none' }); +}; + +export const getExportUrl = Api.exportXls; +export const getImportUrl = Api.importExcel; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts new file mode 100644 index 0000000..061528a --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategy.data.ts @@ -0,0 +1,153 @@ +import { BasicColumn, FormSchema } from '/@/components/Table'; + +export const columns: BasicColumn[] = [ + { title: '密炼物料名称', align: 'center', dataIndex: 'mixerMaterialName', width: 140 }, + { title: '供应商名称', align: 'center', dataIndex: 'supplierName', width: 140 }, + { title: '物料规格', align: 'center', dataIndex: 'materialSpec', width: 120 }, + { title: '包装物重量', align: 'center', dataIndex: 'tareWeight', width: 110 }, + { title: '托盘重量', align: 'center', dataIndex: 'palletWeight', width: 100 }, + { title: '单位', align: 'center', dataIndex: 'unitName', width: 80 }, + { title: '生效开始日期', align: 'center', dataIndex: 'effectiveStartDate', width: 120, customRender: ({ text }) => (text ? String(text).substring(0, 10) : '') }, + { title: '生效截止日期', align: 'center', dataIndex: 'effectiveEndDate', width: 120, customRender: ({ text }) => (text ? String(text).substring(0, 10) : '') }, + { title: '维护人', align: 'center', dataIndex: 'maintainBy_dictText', width: 100 }, + { title: '创建时间', align: 'center', dataIndex: 'createTime', width: 165 }, +]; + +export const searchFormSchema: FormSchema[] = [ + { + label: '密炼物料', + field: 'mixerMaterialId', + component: 'JDictSelectTag', + componentProps: { + dictCode: 'mes_mixer_material,material_name,id', + placeholder: '请选择密炼物料', + }, + colProps: { span: 6 }, + }, + { label: '密炼物料名称', field: 'mixerMaterialName', component: 'JInput', colProps: { span: 6 } }, + { + label: '供应商', + field: 'supplierId', + component: 'JDictSelectTag', + componentProps: { + dictCode: 'mes_xsl_supplier,supplier_name,id', + placeholder: '请选择供应商', + }, + colProps: { span: 6 }, + }, + { label: '供应商名称', field: 'supplierName', component: 'JInput', colProps: { span: 6 } }, + { label: '物料规格', field: 'materialSpec', component: 'JInput', colProps: { span: 6 } }, + { + label: '生效日期', + field: 'effectiveDateRange', + component: 'RangePicker', + componentProps: { + valueFormat: 'YYYY-MM-DD', + placeholder: ['开始日期', '截止日期'], + }, + colProps: { span: 8 }, + }, +]; + +export const formSchema: FormSchema[] = [ + { label: '', field: 'id', component: 'Input', show: false }, + { + label: '密炼物料', + field: 'mixerMaterialName', + component: 'Input', + slot: 'mixerMaterialPicker', + dynamicRules: () => [{ required: true, message: '请选择密炼物料' }], + }, + { label: '', field: 'mixerMaterialId', component: 'Input', show: false }, + { + label: '供应商', + field: 'supplierName', + component: 'Input', + slot: 'supplierPicker', + dynamicRules: () => [{ required: true, message: '请选择供应商' }], + }, + { label: '', field: 'supplierId', component: 'Input', show: false }, + { + label: '物料规格', + field: 'materialSpec', + component: 'Input', + componentProps: { placeholder: '请输入物料规格', maxlength: 200 }, + helpMessage: '同一租户/供应商/密炼物料下,不同规格可分别维护;规格相同且生效日期重叠时不允许重复', + colProps: { span: 24 }, + }, + { + label: '包装物重量', + field: 'tareWeight', + component: 'InputNumber', + componentProps: { min: 0, precision: 3, style: { width: '100%' }, placeholder: '请输入包装物重量' }, + dynamicRules: () => [{ required: true, message: '请填写包装物重量' }], + colProps: { span: 12 }, + }, + { + label: '托盘重量', + field: 'palletWeight', + component: 'InputNumber', + componentProps: { min: 0, precision: 3, style: { width: '100%' }, placeholder: '请输入托盘重量' }, + colProps: { span: 12 }, + }, + { + label: '单位', + field: 'unitName', + component: 'Input', + slot: 'unitPicker', + dynamicRules: () => [{ required: true, message: '请选择单位' }], + colProps: { span: 12 }, + }, + { label: '', field: 'unitId', component: 'Input', show: false }, + { + label: '生效开始日期', + field: 'effectiveStartDate', + component: 'DatePicker', + componentProps: { valueFormat: 'YYYY-MM-DD', style: { width: '100%' }, placeholder: '请选择开始日期' }, + dynamicRules: () => [{ required: true, message: '请选择生效开始日期' }], + colProps: { span: 12 }, + }, + { + label: '生效截止日期', + field: 'effectiveEndDate', + component: 'DatePicker', + componentProps: { valueFormat: 'YYYY-MM-DD', style: { width: '100%' }, placeholder: '请选择截止日期' }, + dynamicRules: () => [{ required: true, message: '请选择生效截止日期' }], + colProps: { span: 12 }, + }, + { + label: '维护人', + field: 'maintainBy', + component: 'Input', + componentProps: { disabled: true, placeholder: '保存时自动带出当前登录用户' }, + colProps: { span: 12 }, + }, +]; + +export const superQuerySchema = { + mixerMaterialId: { + title: '密炼物料', + order: 0, + view: 'sel_search', + dictTable: 'mes_mixer_material', + dictCode: 'id', + dictText: 'material_name', + }, + mixerMaterialName: { title: '密炼物料名称', order: 1, view: 'text' }, + supplierId: { + title: '供应商', + order: 2, + view: 'sel_search', + dictTable: 'mes_xsl_supplier', + dictCode: 'id', + dictText: 'supplier_name', + }, + supplierName: { title: '供应商名称', order: 3, view: 'text' }, + materialSpec: { title: '物料规格', order: 4, view: 'text' }, + tareWeight: { title: '包装物重量', order: 5, view: 'number' }, + palletWeight: { title: '托盘重量', order: 6, view: 'number' }, + unitName: { title: '单位', order: 7, view: 'text' }, + effectiveStartDate: { title: '生效开始日期', order: 8, view: 'date' }, + effectiveEndDate: { title: '生效截止日期', order: 9, view: 'date' }, + maintainBy: { title: '维护人', order: 10, view: 'text' }, +}; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategyList.vue b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategyList.vue new file mode 100644 index 0000000..5fcc2ba --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/MesXslMixerMaterialTareStrategyList.vue @@ -0,0 +1,153 @@ + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue new file mode 100644 index 0000000..98295fc --- /dev/null +++ b/jeecgboot-vue3/src/views/xslmes/mesXslMixerMaterialTareStrategy/components/MesXslMixerMaterialTareStrategyModal.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts index 80e21bc..0fd440a 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialCard/MesXslRawMaterialCard.data.ts @@ -54,6 +54,18 @@ export const columns: BasicColumn[] = [ dataIndex: 'totalWeight', width: 90, }, + { + title: '包装物皮重', + align: 'center', + dataIndex: 'packagingTare', + width: 100, + }, + { + title: '托盘重量', + align: 'center', + dataIndex: 'palletWeight', + width: 90, + }, { title: '剩余重量', align: 'center', @@ -238,6 +250,20 @@ export const formSchemaAdd: FormSchema[] = [ componentProps: { placeholder: '请输入总重', precision: 3 }, colProps: { span: 12 }, }, + { + label: '包装物皮重', + field: 'packagingTare', + component: 'InputNumber', + componentProps: { disabled: true, precision: 3, style: { width: '100%' } }, + colProps: { span: 12 }, + }, + { + label: '托盘重量', + field: 'palletWeight', + component: 'InputNumber', + componentProps: { disabled: true, precision: 3, style: { width: '100%' } }, + colProps: { span: 12 }, + }, { label: '剩余重量', field: 'remainingWeight', @@ -321,6 +347,20 @@ export const formSchemaEdit: FormSchema[] = [ componentProps: { disabled: true }, colProps: { span: 24 }, }, + { + label: '包装物皮重', + field: 'packagingTare', + component: 'InputNumber', + componentProps: { disabled: true, precision: 3, style: { width: '100%' } }, + colProps: { span: 24 }, + }, + { + label: '托盘重量', + field: 'palletWeight', + component: 'InputNumber', + componentProps: { disabled: true, precision: 3, style: { width: '100%' } }, + colProps: { span: 24 }, + }, { label: '剩余数量', field: 'remainingQuantity', @@ -352,10 +392,12 @@ export const superQuerySchema = { materialName: { title: '物料名称', order: 3, view: 'text' }, supplierName: { title: '供应商名称', order: 4, view: 'text' }, totalWeight: { title: '总重', order: 5, view: 'number' }, - remainingWeight: { title: '剩余重量', order: 6, view: 'number' }, - remainingQuantity: { title: '剩余数量', order: 7, view: 'number' }, - status: { title: '状态', order: 8, view: 'list', dictCode: 'xslmes_card_status' }, - testResult: { title: '检测结果', order: 9, view: 'list', dictCode: 'xslmes_test_result' }, - warehouseArea: { title: '库区', order: 10, view: 'text' }, - createTime: { title: '创建时间', order: 11, view: 'datetime' }, + packagingTare: { title: '包装物皮重', order: 6, view: 'number' }, + palletWeight: { title: '托盘重量', order: 7, view: 'number' }, + remainingWeight: { title: '剩余重量', order: 8, view: 'number' }, + remainingQuantity: { title: '剩余数量', order: 9, view: 'number' }, + status: { title: '状态', order: 10, view: 'list', dictCode: 'xslmes_card_status' }, + testResult: { title: '检测结果', order: 11, view: 'list', dictCode: 'xslmes_test_result' }, + warehouseArea: { title: '库区', order: 12, view: 'text' }, + createTime: { title: '创建时间', order: 13, view: 'datetime' }, }; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts index 9851deb..ce0e07a 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialEntry/MesXslRawMaterialEntry.data.ts @@ -17,8 +17,11 @@ export const columns: BasicColumn[] = [ { title: '厂家物料名称', align: 'center', dataIndex: 'manufacturerMaterialName', width: 140, ellipsis: true }, { title: '保质期', align: 'center', dataIndex: 'shelfLife', width: 100 }, { title: '总重(KG)', align: 'center', dataIndex: 'totalWeight', width: 100 }, + { title: '托盘及皮重(合计)', align: 'center', dataIndex: 'palletTareTotal', width: 120 }, { title: '总份数', align: 'center', dataIndex: 'totalPortions', width: 80 }, { title: '每份总重(KG)', align: 'center', dataIndex: 'portionWeight', width: 110 }, + { title: '包装物皮重', align: 'center', dataIndex: 'portionPackagingTare', width: 110, ellipsis: true }, + { title: '托盘重量', align: 'center', dataIndex: 'portionPalletWeight', width: 100, ellipsis: true }, { title: '每份包数', align: 'center', dataIndex: 'portionPackages', width: 80 }, { title: '检测结果', align: 'center', dataIndex: 'testResult_dictText', width: 90 }, { title: '检测状态', align: 'center', dataIndex: 'testStatus_dictText', width: 90 }, @@ -155,6 +158,12 @@ export const formSchema: FormSchema[] = [ component: 'InputNumber', componentProps: { min: 0, precision: 2, placeholder: '请输入总重', style: { width: '100%' } }, }, + { + label: '托盘及皮重(合计)', + field: 'palletTareTotal', + component: 'InputNumber', + componentProps: { disabled: true, precision: 3, style: { width: '100%' } }, + }, { // 字段升级为字符串类型,支持桌面端拆码明细多行拼接(如 20/1/) label: '总份数', @@ -168,6 +177,18 @@ export const formSchema: FormSchema[] = [ component: 'Input', componentProps: { placeholder: '请输入每份总重(多行明细用 / 拼接)', style: { width: '100%' } }, }, + { + label: '包装物皮重', + field: 'portionPackagingTare', + component: 'Input', + componentProps: { disabled: true, placeholder: '拆码明细拼接(/ 分隔)' }, + }, + { + label: '托盘重量', + field: 'portionPalletWeight', + component: 'Input', + componentProps: { disabled: true, placeholder: '拆码明细拼接(/ 分隔)' }, + }, { label: '每份包数', field: 'portionPackages', @@ -258,7 +279,8 @@ export const superQuerySchema = { materialName: { title: '物料名称', order: 4, view: 'text' }, supplierName: { title: '供应商名称', order: 5, view: 'text' }, totalWeight: { title: '总重(KG)', order: 6, view: 'number' }, - testStatus: { title: '检测状态', order: 7, view: 'list', dictCode: 'xslmes_test_status' }, - isSpecialAdoption: { title: '是否特采', order: 8, view: 'list', dictCode: 'yn' }, - status: { title: '状态', order: 9, view: 'list', dictCode: 'xslmes_entry_status' }, + palletTareTotal: { title: '托盘及皮重(合计)', order: 7, view: 'number' }, + testStatus: { title: '检测状态', order: 8, view: 'list', dictCode: 'xslmes_test_status' }, + isSpecialAdoption: { title: '是否特采', order: 9, view: 'list', dictCode: 'yn' }, + status: { title: '状态', order: 10, view: 'list', dictCode: 'xslmes_entry_status' }, }; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialWorkshopRemain/MesXslRawMaterialWorkshopRemain.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialWorkshopRemain/MesXslRawMaterialWorkshopRemain.data.ts index b9899d5..7535a85 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialWorkshopRemain/MesXslRawMaterialWorkshopRemain.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslRawMaterialWorkshopRemain/MesXslRawMaterialWorkshopRemain.data.ts @@ -15,6 +15,8 @@ export const columns: BasicColumn[] = [ { title: '供应商', align: 'center', dataIndex: 'supplierName', width: 160, ellipsis: true }, { title: '保质期', align: 'center', dataIndex: 'shelfLife', width: 120 }, { title: '总重', align: 'center', dataIndex: 'totalWeight', width: 110 }, + { title: '包装物皮重', align: 'center', dataIndex: 'packagingTare', width: 110 }, + { title: '托盘重量', align: 'center', dataIndex: 'palletWeight', width: 100 }, { title: '剩余重量', align: 'center', dataIndex: 'remainingWeight', width: 110 }, { title: '剩余数量', align: 'center', dataIndex: 'remainingQuantity', width: 110 }, { title: '检测结果', align: 'center', dataIndex: 'testResult_dictText', width: 110 }, @@ -64,9 +66,11 @@ export const superQuerySchema = { supplierName: { title: '供应商', order: 4, view: 'text' }, shelfLife: { title: '保质期', order: 5, view: 'text' }, totalWeight: { title: '总重', order: 6, view: 'number' }, - remainingWeight: { title: '剩余重量', order: 7, view: 'number' }, - remainingQuantity: { title: '剩余数量', order: 8, view: 'number' }, - testResult: { title: '检测结果', order: 9, view: 'list', dictCode: 'xslmes_test_result' }, - status: { title: '状态', order: 10, view: 'list', dictCode: 'xslmes_card_status' }, - priorityPickup: { title: '优先使用', order: 11, view: 'list', dictCode: 'yn' }, + packagingTare: { title: '包装物皮重', order: 7, view: 'number' }, + palletWeight: { title: '托盘重量', order: 8, view: 'number' }, + remainingWeight: { title: '剩余重量', order: 9, view: 'number' }, + remainingQuantity: { title: '剩余数量', order: 10, view: 'number' }, + testResult: { title: '检测结果', order: 11, view: 'list', dictCode: 'xslmes_test_result' }, + status: { title: '状态', order: 12, view: 'list', dictCode: 'xslmes_card_status' }, + priorityPickup: { title: '优先使用', order: 13, view: 'list', dictCode: 'yn' }, }; diff --git a/jeecgboot-vue3/src/views/xslmes/mesXslWeightRecord/MesXslWeightRecord.data.ts b/jeecgboot-vue3/src/views/xslmes/mesXslWeightRecord/MesXslWeightRecord.data.ts index 21f0aee..c2a94a9 100644 --- a/jeecgboot-vue3/src/views/xslmes/mesXslWeightRecord/MesXslWeightRecord.data.ts +++ b/jeecgboot-vue3/src/views/xslmes/mesXslWeightRecord/MesXslWeightRecord.data.ts @@ -31,6 +31,30 @@ export const columns: BasicColumn[] = [ return Number.isInteger(n) ? String(n) : n.toFixed(2); }, }, + { + title: '货物皮重', + align: 'center', + dataIndex: 'cargoTareWeight', + width: 100, + customRender: ({ text }) => { + if (text === null || text === undefined || text === '') return '0'; + const n = Number(text); + if (Number.isNaN(n)) return String(text); + return Number.isInteger(n) ? String(n) : n.toFixed(2); + }, + }, + { + title: '原料重量', + align: 'center', + dataIndex: 'rawMaterialWeight', + width: 100, + customRender: ({ text }) => { + if (text === null || text === undefined || text === '') return ''; + const n = Number(text); + if (Number.isNaN(n)) return String(text); + return Number.isInteger(n) ? String(n) : n.toFixed(2); + }, + }, { title: '司机', align: 'center', dataIndex: 'driverName', width: 90 }, { title: '手机号', align: 'center', dataIndex: 'driverPhone', width: 120 }, ]; diff --git a/jeecgboot-vue3/tsconfig.json b/jeecgboot-vue3/tsconfig.json index a8ac696..ef9069b 100644 --- a/jeecgboot-vue3/tsconfig.json +++ b/jeecgboot-vue3/tsconfig.json @@ -21,6 +21,7 @@ "typeRoots": ["./node_modules/@types/", "./types","./node_modules"], "noImplicitAny": false, "skipLibCheck": true, + "noEmit": true, "paths": { "/@/*": ["src/*"], "/#/*": ["types/*"], diff --git a/scan_tare_strategy.json b/scan_tare_strategy.json new file mode 100644 index 0000000..2ad2b63 --- /dev/null +++ b/scan_tare_strategy.json @@ -0,0 +1,3 @@ +{ + "error": "未找到匹配 'MesXslMixerMaterialTareStrategy' 的实体类,建议使用完整类名如 MesXslVehicle" +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/MixerMaterialTareStrategyChangedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/MixerMaterialTareStrategyChangedEvent.cs new file mode 100644 index 0000000..c6e442c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/MixerMaterialTareStrategyChangedEvent.cs @@ -0,0 +1,11 @@ +using Prism.Events; + +namespace YY.Admin.Core.Events; + +public class MixerMaterialTareStrategyChangedPayload +{ + public string Action { get; set; } = string.Empty; + public string? TareStrategyId { get; set; } +} + +public class MixerMaterialTareStrategyChangedEvent : PubSubEvent { } diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IMixerMaterialTareStrategyService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IMixerMaterialTareStrategyService.cs new file mode 100644 index 0000000..748d1e8 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IMixerMaterialTareStrategyService.cs @@ -0,0 +1,28 @@ +using YY.Admin.Core.Entity; + +namespace YY.Admin.Core.Services; + +public interface IMixerMaterialTareStrategyService +{ + Task PageAsync( + int pageNo, + int pageSize, + string? mixerMaterialName = null, + string? supplierName = null, + CancellationToken ct = default); + + Task GetByIdAsync(string id, CancellationToken ct = default); + Task AddAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default); + Task EditAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default); + Task DeleteAsync(string id, CancellationToken ct = default); + Task> GetUnitsAsync(CancellationToken ct = default); + + ///

拉取全部策略(用于原料入场拆码明细自动/手动匹配)。 + Task> GetAllForMatchAsync(CancellationToken ct = default); +} + +public record MixerMaterialTareStrategyPageResult( + List Records, + long Total, + int PageNo, + int PageSize); diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslMixerMaterialTareStrategy.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslMixerMaterialTareStrategy.cs new file mode 100644 index 0000000..2471d99 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslMixerMaterialTareStrategy.cs @@ -0,0 +1,31 @@ +namespace YY.Admin.Core.Entity; + +public class MesXslMixerMaterialTareStrategy +{ + public string? Id { get; set; } + public int? TenantId { get; set; } + public string? MixerMaterialId { get; set; } + public string? MixerMaterialName { get; set; } + public string? SupplierId { get; set; } + public string? SupplierName { get; set; } + public string? MaterialSpec { get; set; } + public decimal? TareWeight { get; set; } + public decimal? PalletWeight { get; set; } + public string? UnitId { get; set; } + public string? UnitName { get; set; } + public DateTime? EffectiveStartDate { get; set; } + public DateTime? EffectiveEndDate { get; set; } + public string? MaintainBy { get; set; } + public string? CreateBy { get; set; } + public DateTime? CreateTime { get; set; } + public string? UpdateBy { get; set; } + public DateTime? UpdateTime { get; set; } + public string? SysOrgCode { get; set; } + public int? DelFlag { get; set; } + + public string EffectiveStartDateText => + EffectiveStartDate?.ToString("yyyy-MM-dd") ?? string.Empty; + + public string EffectiveEndDateText => + EffectiveEndDate?.ToString("yyyy-MM-dd") ?? string.Empty; +} diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs index 89fc811..b8a4d53 100644 --- a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialCard.cs @@ -17,6 +17,10 @@ public class MesXslRawMaterialCard public string? ManufacturerMaterialName { get; set; } public string? ShelfLife { get; set; } public decimal? TotalWeight { get; set; } + /// 包装物皮重(KG) + public decimal? PackagingTare { get; set; } + /// 托盘重量(KG) + public decimal? PalletWeight { get; set; } public decimal? RemainingWeight { get; set; } public int? RemainingQuantity { get; set; } diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs index 215f26c..f19ac03 100644 --- a/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslRawMaterialEntry.cs @@ -17,11 +17,15 @@ public class MesXslRawMaterialEntry public string? ManufacturerMaterialName { get; set; } public string? ShelfLife { get; set; } public double? TotalWeight { get; set; } + public double? PalletTareTotal { get; set; } // 总份数 / 每份总重 / 每份包数:与后端同步升级为字符串, // 用于持久化「拆码明细」多行拼接(如 20/1/、100/200/)。 public string? TotalPortions { get; set; } public string? PortionWeight { get; set; } + public string? PortionPackagingTare { get; set; } + public string? PortionPalletWeight { get; set; } + public string? PortionTareStrategyIds { get; set; } public string? PortionPackages { get; set; } // 拆码明细各行库位的拼接(以 / 分隔,末尾带 /,如 1F-A01/1F-A02/)。 // 与 WarehouseLocation(基础资料整票级单值)独立,专供明细行回填。 diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslUnit.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslUnit.cs new file mode 100644 index 0000000..62c0442 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslUnit.cs @@ -0,0 +1,9 @@ +namespace YY.Admin.Core.Entity; + +public class MesXslUnit +{ + public string? Id { get; set; } + public string? UnitCode { get; set; } + public string? UnitName { get; set; } + public int? TenantId { get; set; } +} diff --git a/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs b/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs index e2f4976..28eac3c 100644 --- a/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs +++ b/yy-admin-master/YY.Admin.Core/Entity/MesXslWeightRecord.cs @@ -40,6 +40,17 @@ public class MesXslWeightRecord /// public double? EnteredWeight { get; set; } + /// + /// 货物皮重(KG) —— 后端/本地实时计算,不入库。 + /// 来源:所有引用本榜单(BillNo)的原料入场记录 pallet_tare_total(托盘及皮重合计)累加。 + /// + public double? CargoTareWeight { get; set; } + + /// + /// 原料重量(KG) —— 实时计算,不入库。公式:净重 - 货物皮重。 + /// + public double? RawMaterialWeight { get; set; } + /// 司机姓名 public string? DriverName { get; set; } diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs index e50cc67..dfb1c1d 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs @@ -46,6 +46,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData new SysMenu{ Id=1300150010901, Pid=1300150000101, Title="原材料卡片", Path="/xslmes/mesXslRawMaterialCard", Name="mesXslRawMaterialCard", Component="RawMaterialCardListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=108 }, // 库区管理 new SysMenu{ Id=1300150011001, Pid=1300150000101, Title="库区管理", Path="/xslmes/mesXslWarehouseArea", Name="mesXslWarehouseArea", Component="WarehouseAreaListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=109 }, + // 密炼物料皮重策略 + new SysMenu{ Id=1300150011101, Pid=1300150000101, Title="密炼物料皮重策略", Path="/xslmes/mesXslMixerMaterialTareStrategy", Name="mesXslMixerMaterialTareStrategy", Component="MixerMaterialTareStrategyListView", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, #endregion diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs index b6eaecb..b3b9d36 100644 --- a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs @@ -31,6 +31,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010801}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150010901}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011001}, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300150011101}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012101}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012111}, new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200012121}, diff --git a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs index 57f85f4..f990bb4 100644 --- a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs +++ b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs @@ -268,6 +268,8 @@ namespace YY.Admin.Core.SqlSugar if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config); // 关闭全量种子时首启可能无菜单数据;补一份基准菜单,避免打包版本左侧空白 EnsureBaselineSysMenuSeed(db, config); + // 旧库升级:按种子补全缺失菜单及租户/角色授权(仅插入缺失项) + EnsureIncrementalDesktopMenuSeed(db, config); } /// @@ -392,6 +394,106 @@ namespace YY.Admin.Core.SqlSugar } } + /// + /// 旧库升级:sys_menu 已有数据时,按 SysMenuSeedData 补全缺失菜单,并同步租户菜单、管理员角色菜单授权。 + /// + private static void EnsureIncrementalDesktopMenuSeed(SqlSugarScope db, DbConnectionConfig config) + { + try + { + if (!string.Equals(config.ConfigId.ToString(), SqlSugarConst.MainConfigId, StringComparison.Ordinal)) + { + return; + } + + if (config.DbType != DbType.Sqlite) + { + return; + } + + var dbProvider = db.GetConnectionScope(config.ConfigId); + var menuEntityInfo = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysMenu)); + if (!dbProvider.DbMaintenance.IsAnyTable(menuEntityInfo.DbTableName, false)) + { + return; + } + + if (dbProvider.Queryable().ClearFilter().Count() == 0) + { + return; + } + + var seedMenus = new SysMenuSeedData().HasData().ToList(); + var existingMenuIds = dbProvider.Queryable().ClearFilter() + .Select(m => m.Id).ToList().ToHashSet(); + var missingMenus = seedMenus.Where(m => !existingMenuIds.Contains(m.Id)).ToList(); + if (missingMenus.Count == 0) + { + return; + } + + foreach (var menu in missingMenus) + { + if (menu.CreateTime == default) + { + menu.CreateTime = DateTime.Parse("2022-02-10 00:00:00"); + } + + menu.Status = StatusEnum.Enable; + } + + dbProvider.Insertable(missingMenus).ExecuteCommand(); + + const long defaultTenantId = 1300000000001; + var tenantSeed = new SysTenantMenuSeedData().HasData() + .Where(t => t.TenantId == defaultTenantId) + .ToList(); + var existingTenantMenuIds = dbProvider.Queryable() + .Where(t => t.TenantId == defaultTenantId) + .Select(t => t.MenuId) + .ToList() + .ToHashSet(); + + var tenantMenusToInsert = tenantSeed + .Where(t => missingMenus.Any(m => m.Id == t.MenuId) && !existingTenantMenuIds.Contains(t.MenuId)) + .Select(t => new SysTenantMenu { Id = t.MenuId, TenantId = t.TenantId, MenuId = t.MenuId }) + .ToList(); + if (tenantMenusToInsert.Count > 0) + { + dbProvider.Insertable(tenantMenusToInsert).ExecuteCommand(); + } + + var adminRole = dbProvider.Queryable().OrderBy(r => r.Id).First(); + if (adminRole == null) + { + return; + } + + var existingRoleMenuIds = dbProvider.Queryable() + .Where(r => r.RoleId == adminRole.Id) + .Select(r => r.MenuId) + .ToList() + .ToHashSet(); + var roleMenusToInsert = missingMenus + .Where(m => !existingRoleMenuIds.Contains(m.Id)) + .Select(m => new SysRoleMenu + { + Id = m.Id + (adminRole.Id % 1300000000000), + RoleId = adminRole.Id, + MenuId = m.Id, + }) + .ToList(); + if (roleMenusToInsert.Count > 0) + { + dbProvider.Insertable(roleMenusToInsert).ExecuteCommand(); + } + } + catch + { + // 启动阶段不因增量菜单失败而阻断 + } + } + /// /// 兼容旧库:补齐桌面端「登录设置」所需的 sys_config 配置项(升级前库可能缺少这些 code) /// diff --git a/yy-admin-master/YY.Admin.Core/Util/CargoTareWeightCalculator.cs b/yy-admin-master/YY.Admin.Core/Util/CargoTareWeightCalculator.cs new file mode 100644 index 0000000..23f815c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/CargoTareWeightCalculator.cs @@ -0,0 +1,49 @@ +using YY.Admin.Core.Entity; + +namespace YY.Admin.Core.Util; + +/// +/// 「货物皮重」桌面端本地累计计算器。 +/// 与后端 IMesXslRawMaterialEntryService.sumCargoTareByBillNos 保持同一口径: +/// 同一榜单(BillNo)下所有原料入场记录的 PalletTareTotal(托盘及皮重合计)累加。 +/// +public static class CargoTareWeightCalculator +{ + /// 按 BillNo 分组累计「货物皮重」。 + public static Dictionary SumByBillNos( + IEnumerable entries, + IEnumerable billNos) + { + var keys = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var b in billNos) + { + if (!string.IsNullOrWhiteSpace(b)) keys.Add(b!); + } + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (keys.Count == 0) + { + return result; + } + + foreach (var e in entries) + { + if (string.IsNullOrWhiteSpace(e.BillNo) || !keys.Contains(e.BillNo!)) + { + continue; + } + if (e.PalletTareTotal is not { } tare || tare == 0d) + { + continue; + } + if (result.TryGetValue(e.BillNo!, out var acc)) + { + result[e.BillNo!] = acc + tare; + } + else + { + result[e.BillNo!] = tare; + } + } + return result; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyMatcher.cs b/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyMatcher.cs new file mode 100644 index 0000000..e5aeb38 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyMatcher.cs @@ -0,0 +1,80 @@ +using System.Globalization; +using YY.Admin.Core.Entity; + +namespace YY.Admin.Services.Service.MixerMaterialTareStrategy; + +/// +/// 密炼物料皮重策略匹配:按密炼物料、供应商、入场日期、每份重量(对应策略物料规格)筛选。 +/// +public static class MixerMaterialTareStrategyMatcher +{ + public static List FilterCandidates( + IEnumerable strategies, + string? mixerMaterialId, + string? supplierId, + DateTime? entryDate, + double? portionWeight) + { + if (string.IsNullOrWhiteSpace(mixerMaterialId) || string.IsNullOrWhiteSpace(supplierId)) + { + return new List(); + } + + var entryDay = entryDate?.Date; + return strategies + .Where(s => string.Equals(s.MixerMaterialId, mixerMaterialId, StringComparison.OrdinalIgnoreCase)) + .Where(s => string.Equals(s.SupplierId, supplierId, StringComparison.OrdinalIgnoreCase)) + .Where(s => IsEntryDateInRange(entryDay, s.EffectiveStartDate, s.EffectiveEndDate)) + .Where(s => MaterialSpecMatchesPortionWeight(s.MaterialSpec, portionWeight)) + .OrderByDescending(s => s.EffectiveStartDate ?? DateTime.MinValue) + .ThenByDescending(s => s.CreateTime ?? DateTime.MinValue) + .ToList(); + } + + public static MesXslMixerMaterialTareStrategy? PickBestMatch( + IEnumerable strategies, + string? mixerMaterialId, + string? supplierId, + DateTime? entryDate, + double? portionWeight) + { + var list = FilterCandidates(strategies, mixerMaterialId, supplierId, entryDate, portionWeight); + return list.FirstOrDefault(); + } + + public static bool IsEntryDateInRange(DateTime? entryDay, DateTime? start, DateTime? end) + { + if (!entryDay.HasValue || !start.HasValue || !end.HasValue) + { + return false; + } + + var day = entryDay.Value.Date; + return day >= start.Value.Date && day <= end.Value.Date; + } + + /// 策略「物料规格」与拆码明细「每份重量」比对(支持数值或文本形式)。 + public static bool MaterialSpecMatchesPortionWeight(string? materialSpec, double? portionWeight) + { + if (!portionWeight.HasValue) + { + return string.IsNullOrWhiteSpace(materialSpec); + } + + var weight = portionWeight.Value; + if (string.IsNullOrWhiteSpace(materialSpec)) + { + return false; + } + + var spec = materialSpec.Trim(); + if (double.TryParse(spec, NumberStyles.Float, CultureInfo.InvariantCulture, out var specNum)) + { + return Math.Abs(specNum - weight) < 0.0001d; + } + + var weightText = weight.ToString("0.##", CultureInfo.InvariantCulture); + return string.Equals(spec, weightText, StringComparison.OrdinalIgnoreCase) + || string.Equals(spec, weight.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs b/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs new file mode 100644 index 0000000..13dbb2a --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs @@ -0,0 +1,712 @@ +using Microsoft.Extensions.Configuration; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Web; +using Prism.Events; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; + +namespace YY.Admin.Services.Service.MixerMaterialTareStrategy; + +public class MixerMaterialTareStrategyService : IMixerMaterialTareStrategyService, ISingletonDependency +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly INetworkMonitor _networkMonitor; + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + private readonly SemaphoreSlim _syncLock = new(1, 1); + private readonly object _cacheLock = new(); + private readonly string _pendingOpsFilePath; + private readonly string _cacheFilePath; + private List _pendingOps = new(); + private List _localCache = new(); + + private static readonly JsonSerializerOptions _jsonOpts = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new NullableDateTimeJsonConverter() } + }; + + public MixerMaterialTareStrategyService( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + INetworkMonitor networkMonitor, + IEventAggregator eventAggregator, + ILoggerService logger) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _networkMonitor = networkMonitor; + _eventAggregator = eventAggregator; + _logger = logger; + + var appDataDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "YY.Admin", "sync-cache"); + Directory.CreateDirectory(appDataDir); + _pendingOpsFilePath = Path.Combine(appDataDir, "mes-xsl-tare-strategy-pending-ops.json"); + _cacheFilePath = Path.Combine(appDataDir, "mes-xsl-tare-strategy-cache.json"); + + LoadPendingOpsFromDisk(); + LoadCacheFromDisk(); + _logger.Information($"[密炼物料皮重策略同步] 服务初始化,缓存={_localCache.Count},待上传={_pendingOps.Count},在线={_networkMonitor.IsOnline}"); + + _networkMonitor.StatusChanged += OnNetworkStatusChanged; + if (_networkMonitor.IsOnline) + _ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None)); + } + + private const int MaxPendingRetries = 5; + + private string BaseUrl => (_configuration.GetValue("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); + private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; + + private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi"); + + public async Task PageAsync( + int pageNo, int pageSize, + string? mixerMaterialName = null, string? supplierName = null, + CancellationToken ct = default) + { + List? source = null; + if (_networkMonitor.IsOnline) + { + try + { + source = await FetchRemoteListAsync(ct).ConfigureAwait(false); + lock (_cacheLock) + { + _localCache = source.Select(Clone).ToList(); + SaveCacheToDiskUnsafe(); + } + } + catch (Exception ex) + { + source = null; + _logger.Warning($"[密炼物料皮重策略列表] 远端拉取失败,回退缓存:{ex.Message}"); + } + } + + lock (_cacheLock) + { + source ??= _localCache.Select(Clone).ToList(); + source = ApplyPendingOpsSnapshotUnsafe(source); + } + + var filtered = ApplyFilters(source, mixerMaterialName, supplierName); + var total = filtered.Count; + var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList(); + return new MixerMaterialTareStrategyPageResult(records, total, pageNo, pageSize); + } + + public async Task> GetAllForMatchAsync(CancellationToken ct = default) + { + var page = await PageAsync(1, 10000, null, null, ct).ConfigureAwait(false); + return page.Records; + } + + public async Task GetByIdAsync(string id, CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + if (!resp.IsSuccessStatusCode) return null; + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("result", out var resultEl)) return null; + return resultEl.Deserialize(_jsonOpts); + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略详情] 远端查询失败 id={id},回退缓存:{ex.Message}"); + } + } + + lock (_cacheLock) + { + return _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found + ? Clone(found) : null; + } + } + + public async Task AddAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default) + { + if (!strategy.TenantId.HasValue || strategy.TenantId.Value <= 0) + strategy.TenantId = DefaultTenantId; + + var local = Clone(strategy); + if (string.IsNullOrWhiteSpace(local.Id)) + local.Id = $"local-{Guid.NewGuid():N}"; + + if (_networkMonitor.IsOnline) + { + try + { + var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false); + if (ok) { UpsertLocalCache(local); return true; } + return false; + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略新增] 远端失败,转离线入队:{ex.Message}"); + } + } + + EnqueuePendingOperation(new TareStrategyPendingOperation + { + OpType = TareStrategyOperationType.Add, + TareStrategyId = local.Id, + Strategy = local + }); + UpsertLocalCache(local); + return true; + } + + public async Task EditAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default) + { + if (!strategy.TenantId.HasValue || strategy.TenantId.Value <= 0) + strategy.TenantId = DefaultTenantId; + + var local = Clone(strategy); + if (_networkMonitor.IsOnline) + { + try + { + var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false); + if (ok) { UpsertLocalCache(local); return true; } + return false; + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略编辑] 远端失败,转离线入队:{ex.Message}"); + } + } + + EnqueuePendingOperation(new TareStrategyPendingOperation + { + OpType = TareStrategyOperationType.Edit, + TareStrategyId = local.Id, + Strategy = local, + AnchorUpdateTime = local.UpdateTime + }); + UpsertLocalCache(local); + return true; + } + + public async Task DeleteAsync(string id, CancellationToken ct = default) + { + if (_networkMonitor.IsOnline) + { + try + { + var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false); + if (ok) { RemoveFromLocalCache(id); return true; } + return false; + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略删除] 远端失败,转离线入队:{ex.Message}"); + } + } + + DateTime? anchor; + lock (_cacheLock) + { + anchor = _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime; + } + EnqueuePendingOperation(new TareStrategyPendingOperation + { + OpType = TareStrategyOperationType.Delete, + TareStrategyId = id, + AnchorUpdateTime = anchor + }); + RemoveFromLocalCache(id); + return true; + } + + public async Task> GetUnitsAsync(CancellationToken ct = default) + { + if (!_networkMonitor.IsOnline) + return new List(); + + try + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["pageNo"] = "1"; + query["pageSize"] = "1000"; + query["tenantId"] = DefaultTenantId.ToString(); + var url = $"{BaseUrl}/xslmes/mesXslUnit/anon/list?{query}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.GetProperty("result").GetProperty("records") + .Deserialize>(_jsonOpts) ?? new(); + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略] 单位列表拉取失败:{ex.Message}"); + return new List(); + } + } + + private async Task> FetchRemoteListAsync(CancellationToken ct) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["pageNo"] = "1"; + query["pageSize"] = "10000"; + query["tenantId"] = DefaultTenantId.ToString(); + var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/list?{query}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + return doc.RootElement.GetProperty("result").GetProperty("records") + .Deserialize>(_jsonOpts) ?? new(); + } + + private async Task RemoteAddAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/add?tenantId={DefaultTenantId}"; + var payload = Clone(strategy); + if (IsLocalTempId(payload.Id)) payload.Id = null; + return await PostJsonAsync(url, payload, ct).ConfigureAwait(false); + } + + private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/edit?tenantId={DefaultTenantId}"; + return await PostJsonCheckVersionAsync(url, strategy, ct).ConfigureAwait(false); + } + + private async Task RemoteDeleteAsync(string id, CancellationToken ct) + { + var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false); + return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false); + } + + private async Task PostJsonAsync(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); + return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false); + } + + private async Task<(bool Ok, bool IsVersionConflict)> PostJsonCheckVersionAsync(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, false); + try + { + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + int code = 200; + if (doc.RootElement.TryGetProperty("code", out var codeEl)) code = codeEl.GetInt32(); + if (code == 200) return (true, false); + if (doc.RootElement.TryGetProperty("message", out var msgEl)) + { + var msg = msgEl.GetString() ?? ""; + if (msg.Contains("已被他人修改")) return (false, true); + } + return (false, false); + } + catch { return (true, false); } + } + + private static async Task IsSuccessResultAsync(HttpResponseMessage resp, CancellationToken ct) + { + try + { + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("code", out var code)) return code.GetInt32() == 200; + if (doc.RootElement.TryGetProperty("success", out var success)) return success.GetBoolean(); + return true; + } + catch { return true; } + } + + private void OnNetworkStatusChanged(bool isOnline) + { + if (!isOnline) return; + _ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None)); + } + + private async Task SyncAfterReconnectAsync(CancellationToken ct) + { + var pushResult = await PushPendingOnReconnectAsync(ct).ConfigureAwait(false); + if (!_networkMonitor.IsOnline) return; + try + { + var remote = await FetchRemoteListAsync(ct).ConfigureAwait(false); + lock (_cacheLock) + { + _localCache = remote.Select(Clone).ToList(); + SaveCacheToDiskUnsafe(); + } + _eventAggregator.GetEvent() + .Publish(new MixerMaterialTareStrategyChangedPayload { Action = "pull" }); + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略重连] 全量回拉失败:{ex.Message}"); + } + + var hasActivity = pushResult.PushedCount > 0 || pushResult.ConflictCount > 0 || pushResult.NewRecordsPushed > 0; + if (hasActivity) + { + _eventAggregator.GetEvent() + .Publish(new SyncConflictPayload + { + EntityName = "密炼物料皮重策略", + PushedCount = pushResult.PushedCount, + ConflictCount = pushResult.ConflictCount, + NewRecordsPushed = pushResult.NewRecordsPushed + }); + } + } + + private async Task PushPendingOnReconnectAsync(CancellationToken ct) + { + if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) + return new PushPendingResult(0, 0, 0); + try + { + List snapshot; + lock (_cacheLock) { snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList(); } + + int pushed = 0, conflicts = 0, newPushed = 0; + foreach (var op in snapshot) + { + if (!_networkMonitor.IsOnline) break; + lock (_cacheLock) + { + if (!_pendingOps.Any(x => x.Id == op.Id)) continue; + } + + var result = await ExecutePendingOpWithConflictAsync(op, ct).ConfigureAwait(false); + if (!result.Ok) + { + lock (_cacheLock) + { + op.RetryCount++; + if (op.RetryCount >= MaxPendingRetries) + { + _pendingOps.RemoveAll(x => x.Id == op.Id); + SavePendingOpsToDiskUnsafe(); + continue; + } + SavePendingOpsToDiskUnsafe(); + } + break; + } + + if (result.IsConflict) + { + conflicts++; + if (!string.IsNullOrWhiteSpace(result.EntityId)) + RemovePendingOpsByTareStrategyId(result.EntityId); + } + else if (op.OpType == TareStrategyOperationType.Add) + { + newPushed++; + lock (_cacheLock) + { + _pendingOps.RemoveAll(x => x.Id == op.Id); + SavePendingOpsToDiskUnsafe(); + } + } + else + { + pushed++; + lock (_cacheLock) + { + _pendingOps.RemoveAll(x => x.Id == op.Id); + SavePendingOpsToDiskUnsafe(); + } + } + } + + return new PushPendingResult(pushed, conflicts, newPushed); + } + finally { _syncLock.Release(); } + } + + private sealed record PushPendingResult(int PushedCount, int ConflictCount, int NewRecordsPushed); + private sealed record PendingReplayResult(bool Ok, bool IsConflict, string? EntityId); + + private async Task ExecutePendingOpWithConflictAsync(TareStrategyPendingOperation op, CancellationToken ct) + { + try + { + return op.OpType switch + { + TareStrategyOperationType.Add => await ExecuteAddAsync(op, ct).ConfigureAwait(false), + TareStrategyOperationType.Edit => await ExecuteEditWithConflictAsync(op, ct).ConfigureAwait(false), + TareStrategyOperationType.Delete => await ExecuteDeleteWithConflictAsync(op, ct).ConfigureAwait(false), + _ => new PendingReplayResult(true, false, null) + }; + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略回放] 执行失败 op={op.OpType}:{ex.Message}"); + return new PendingReplayResult(false, false, null); + } + } + + private async Task ExecuteAddAsync(TareStrategyPendingOperation op, CancellationToken ct) + { + var ok = op.Strategy != null && await RemoteAddAsync(op.Strategy, ct).ConfigureAwait(false); + return ok + ? new PendingReplayResult(true, false, op.TareStrategyId) + : new PendingReplayResult(false, false, null); + } + + private async Task ExecuteEditWithConflictAsync(TareStrategyPendingOperation op, CancellationToken ct) + { + var id = op.Strategy?.Id; + if (string.IsNullOrWhiteSpace(id)) + return new PendingReplayResult(false, false, null); + + var remote = await FetchRemoteSingleAsync(id!, ct).ConfigureAwait(false); + if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime) + { + UpsertLocalCache(remote); + return new PendingReplayResult(true, true, id); + } + + var (ok, isVersionConflict) = await RemoteEditAsync(op.Strategy!, ct).ConfigureAwait(false); + if (isVersionConflict) + { + var fresh = await FetchRemoteSingleAsync(id!, ct).ConfigureAwait(false); + if (fresh != null) UpsertLocalCache(fresh); + return new PendingReplayResult(true, true, id); + } + return ok + ? new PendingReplayResult(true, false, id) + : new PendingReplayResult(false, false, null); + } + + private async Task ExecuteDeleteWithConflictAsync(TareStrategyPendingOperation op, CancellationToken ct) + { + if (string.IsNullOrWhiteSpace(op.TareStrategyId)) + return new PendingReplayResult(false, false, null); + + var id = op.TareStrategyId!; + var remote = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false); + if (remote == null) + return new PendingReplayResult(true, false, id); + + if (op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime) + { + UpsertLocalCache(remote); + return new PendingReplayResult(true, true, id); + } + + var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false); + return ok + ? new PendingReplayResult(true, false, id) + : new PendingReplayResult(false, false, null); + } + + private void RemovePendingOpsByTareStrategyId(string tareStrategyId) + { + lock (_cacheLock) + { + _pendingOps.RemoveAll(x => + (x.TareStrategyId != null && string.Equals(x.TareStrategyId, tareStrategyId, StringComparison.OrdinalIgnoreCase)) || + (x.Strategy?.Id != null && string.Equals(x.Strategy.Id, tareStrategyId, StringComparison.OrdinalIgnoreCase))); + SavePendingOpsToDiskUnsafe(); + } + } + + private async Task FetchRemoteSingleAsync(string id, CancellationToken ct) + { + try + { + var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}"; + using var client = CreateClient(); + var resp = await client.GetAsync(url, ct).ConfigureAwait(false); + if (!resp.IsSuccessStatusCode) return null; + var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("result", out var resultEl)) + return resultEl.Deserialize(_jsonOpts); + return null; + } + catch { return null; } + } + + private static List ApplyFilters( + List source, + string? mixerMaterialName, string? supplierName) + { + IEnumerable q = source; + if (!string.IsNullOrWhiteSpace(mixerMaterialName)) + q = q.Where(c => (c.MixerMaterialName ?? "").Contains(mixerMaterialName, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(supplierName)) + q = q.Where(c => (c.SupplierName ?? "").Contains(supplierName, StringComparison.OrdinalIgnoreCase)); + return q.OrderByDescending(c => c.EffectiveStartDate ?? DateTime.MinValue).ToList(); + } + + private List ApplyPendingOpsSnapshotUnsafe(List source) + { + var map = source.Where(c => !string.IsNullOrWhiteSpace(c.Id)) + .ToDictionary(c => c.Id!, Clone, StringComparer.OrdinalIgnoreCase); + foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt)) + { + switch (op.OpType) + { + case TareStrategyOperationType.Add: + case TareStrategyOperationType.Edit: + if (op.Strategy?.Id != null) map[op.Strategy.Id] = Clone(op.Strategy); + break; + case TareStrategyOperationType.Delete: + if (op.TareStrategyId != null) map.Remove(op.TareStrategyId); + break; + } + } + return map.Values.ToList(); + } + + private void EnqueuePendingOperation(TareStrategyPendingOperation op) + { + lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); } + } + + private void UpsertLocalCache(MesXslMixerMaterialTareStrategy strategy) + { + lock (_cacheLock) + { + var idx = _localCache.FindIndex(c => string.Equals(c.Id, strategy.Id, StringComparison.OrdinalIgnoreCase)); + if (idx >= 0) _localCache[idx] = Clone(strategy); + else _localCache.Insert(0, Clone(strategy)); + SaveCacheToDiskUnsafe(); + } + } + + private void RemoveFromLocalCache(string id) + { + lock (_cacheLock) + { + _localCache.RemoveAll(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)); + SaveCacheToDiskUnsafe(); + } + } + + private void LoadPendingOpsFromDisk() + { + try + { + if (!File.Exists(_pendingOpsFilePath)) return; + _pendingOps = JsonSerializer.Deserialize>( + File.ReadAllText(_pendingOpsFilePath), _jsonOpts) ?? new(); + } + catch { _pendingOps = new(); } + } + + private void LoadCacheFromDisk() + { + try + { + if (!File.Exists(_cacheFilePath)) return; + _localCache = JsonSerializer.Deserialize>( + File.ReadAllText(_cacheFilePath), _jsonOpts) ?? new(); + } + catch { _localCache = new(); } + } + + private void SavePendingOpsToDiskUnsafe() => + File.WriteAllText(_pendingOpsFilePath, JsonSerializer.Serialize(_pendingOps, _jsonOpts)); + + private void SaveCacheToDiskUnsafe() => + File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); + + private static bool IsLocalTempId(string? id) => + !string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase); + + private static MesXslMixerMaterialTareStrategy Clone(MesXslMixerMaterialTareStrategy c) => new() + { + Id = c.Id, + TenantId = c.TenantId, + MixerMaterialId = c.MixerMaterialId, + MixerMaterialName = c.MixerMaterialName, + SupplierId = c.SupplierId, + SupplierName = c.SupplierName, + MaterialSpec = c.MaterialSpec, + TareWeight = c.TareWeight, + PalletWeight = c.PalletWeight, + UnitId = c.UnitId, + UnitName = c.UnitName, + EffectiveStartDate = c.EffectiveStartDate, + EffectiveEndDate = c.EffectiveEndDate, + MaintainBy = c.MaintainBy, + CreateBy = c.CreateBy, + CreateTime = c.CreateTime, + UpdateBy = c.UpdateBy, + UpdateTime = c.UpdateTime, + SysOrgCode = c.SysOrgCode, + DelFlag = c.DelFlag + }; + + private sealed class TareStrategyPendingOperation + { + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public TareStrategyOperationType OpType { get; set; } + public string? TareStrategyId { get; set; } + public MesXslMixerMaterialTareStrategy? Strategy { get; set; } + public DateTime? AnchorUpdateTime { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public int RetryCount { get; set; } + } + + private enum TareStrategyOperationType { Add = 1, Edit = 2, Delete = 3 } + + private sealed class NullableDateTimeJsonConverter : JsonConverter + { + private static readonly string[] Formats = + [ + "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.fff", + "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm:ss.fff", + "yyyy-MM-ddTHH:mm:ssZ", "yyyy-MM-ddTHH:mm:ss.fffZ" + ]; + + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) return null; + if (reader.TokenType == JsonTokenType.String) + { + var raw = reader.GetString(); + if (string.IsNullOrWhiteSpace(raw)) return null; + if (DateTime.TryParseExact(raw, Formats, System.Globalization.CultureInfo.InvariantCulture, + System.Globalization.DateTimeStyles.AssumeLocal, out var dt)) return dt; + if (DateTime.TryParse(raw, out var fb)) return fb; + } + throw new JsonException($"无法转换为 DateTime?,token={reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) + { + if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss")); + else writer.WriteNullValue(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategySyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategySyncCoordinator.cs new file mode 100644 index 0000000..f6b002b --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategySyncCoordinator.cs @@ -0,0 +1,72 @@ +using Prism.Events; +using System.Text.Json; +using YY.Admin.Core; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; + +namespace YY.Admin.Services.Service.MixerMaterialTareStrategy; + +public class MixerMaterialTareStrategySyncCoordinator : ISingletonDependency +{ + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + + public MixerMaterialTareStrategySyncCoordinator( + IEventAggregator eventAggregator, + SyncPollManager pollManager, + ILoggerService logger) + { + _eventAggregator = eventAggregator; + _logger = logger; + _eventAggregator.GetEvent() + .Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent() + .Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread); + + pollManager.Register("密炼物料皮重策略", () => + { + _eventAggregator.GetEvent() + .Publish(new MixerMaterialTareStrategyChangedPayload { Action = "poll" }); + return Task.CompletedTask; + }); + + _logger.Information("[密炼物料皮重策略推送] MixerMaterialTareStrategySyncCoordinator 已启动"); + } + + private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload) + { + if (!payload.IsOnline) return; + _logger.Information("[密炼物料皮重策略推送] 网络恢复,触发补偿刷新"); + _eventAggregator.GetEvent() + .Publish(new MixerMaterialTareStrategyChangedPayload { Action = "reconnect" }); + } + + private void OnRemoteCommand(RemoteCommandPayload payload) + { + try + { + var json = payload.CommandJson ?? string.Empty; + if (string.IsNullOrWhiteSpace(json)) return; + + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("cmd", out var cmdEl)) return; + if (!cmdEl.GetString().Equals("MES_MIXER_MATERIAL_TARE_STRATEGY_CHANGED", StringComparison.OrdinalIgnoreCase)) + return; + + doc.RootElement.TryGetProperty("action", out var actionEl); + doc.RootElement.TryGetProperty("tareStrategyId", out var idEl); + + var changed = new MixerMaterialTareStrategyChangedPayload + { + Action = actionEl.GetString() ?? string.Empty, + TareStrategyId = idEl.ValueKind == JsonValueKind.String ? idEl.GetString() : null + }; + _logger.Information($"[密炼物料皮重策略推送] action={changed.Action}, tareStrategyId={changed.TareStrategyId}"); + _eventAggregator.GetEvent().Publish(changed); + } + catch (Exception ex) + { + _logger.Warning($"[密炼物料皮重策略推送] 处理STOMP命令失败:{ex.Message}"); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs index c977298..422600d 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialCard/RawMaterialCardService.cs @@ -1003,6 +1003,8 @@ public class RawMaterialCardService : IRawMaterialCardService, ISingletonDepende ManufacturerMaterialName = input.ManufacturerMaterialName, ShelfLife = input.ShelfLife, TotalWeight = input.TotalWeight, + PackagingTare = input.PackagingTare, + PalletWeight = input.PalletWeight, RemainingWeight = input.RemainingWeight, RemainingQuantity = input.RemainingQuantity, Status = input.Status, diff --git a/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs b/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs index 5cf493e..8f56bee 100644 --- a/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/RawMaterialEntry/RawMaterialEntryService.cs @@ -894,8 +894,13 @@ public class RawMaterialEntryService : IRawMaterialEntryService, ISingletonDepen WeightRecordId = e.WeightRecordId, BillNo = e.BillNo, MaterialId = e.MaterialId, MaterialName = e.MaterialName, SupplyCustomer = e.SupplyCustomer, SupplierId = e.SupplierId, SupplierName = e.SupplierName, ManufacturerMaterialName = e.ManufacturerMaterialName, - ShelfLife = e.ShelfLife, TotalWeight = e.TotalWeight, TotalPortions = e.TotalPortions, - PortionWeight = e.PortionWeight, PortionPackages = e.PortionPackages, + ShelfLife = e.ShelfLife, TotalWeight = e.TotalWeight, PalletTareTotal = e.PalletTareTotal, + TotalPortions = e.TotalPortions, + PortionWeight = e.PortionWeight, + PortionPackagingTare = e.PortionPackagingTare, + PortionPalletWeight = e.PortionPalletWeight, + PortionTareStrategyIds = e.PortionTareStrategyIds, + PortionPackages = e.PortionPackages, PortionWarehouseLocations = e.PortionWarehouseLocations, PortionDetailIds = e.PortionDetailIds, PortionCardFlags = e.PortionCardFlags, diff --git a/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs b/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs index b0b409a..3006d3b 100644 --- a/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs +++ b/yy-admin-master/YY.Admin.Services/Service/WeightRecord/WeightRecordService.cs @@ -113,18 +113,15 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency var filtered = ApplyFilters(source, filterBillNo, filterPlateNumber, filterInoutDirection, filterDriverName, filterMixerMaterialName); var total = filtered.Count; var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList(); - // 当前页结果按本地入场记录缓存,按 BillNo 实时累计「已入场重量」(与后端口径一致)。 - // 放在分页之后做:避免对全量 source 做不必要的计算。 - FillEnteredWeightFromLocalEntries(records); + // 当前页结果按本地入场记录缓存,按 BillNo 实时累计「已入场重量」「货物皮重」(与后端口径一致)。 + FillEntryDerivedFieldsFromLocalEntries(records); return new WeightRecordPageResult(records, total, pageNo, pageSize); } /// - /// 给一批磅单记录批量填充「已入场重量」。 - /// 数据来源:本地 RawMaterialEntry 缓存的拆码明细字段(totalPortions / portionWeight)。 - /// 与后端 sumEnteredWeightByBillNos 同口径,确保离线场景也能正确显示。 + /// 给一批磅单记录批量填充由原料入场记录衍生的 transient 字段。 /// - private void FillEnteredWeightFromLocalEntries(List records) + private void FillEntryDerivedFieldsFromLocalEntries(List records) { if (records.Count == 0) { @@ -137,22 +134,38 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency .ToList(); if (billNos.Count == 0) { - // 全部磅单都没有 BillNo:保留服务端返回值(若有),避免无端置 0。 + foreach (var r in records) + { + ApplyRawMaterialWeight(r); + } return; } var entries = _rawMaterialEntryService.GetCachedSnapshot(); - var sumMap = EnteredWeightCalculator.SumByBillNos(entries, billNos!); + var enteredMap = EnteredWeightCalculator.SumByBillNos(entries, billNos!); + var cargoTareMap = CargoTareWeightCalculator.SumByBillNos(entries, billNos!); foreach (var r in records) { if (string.IsNullOrWhiteSpace(r.BillNo)) { r.EnteredWeight = 0d; + r.CargoTareWeight = 0d; + ApplyRawMaterialWeight(r); continue; } - r.EnteredWeight = sumMap.TryGetValue(r.BillNo, out var v) ? v : 0d; + r.EnteredWeight = enteredMap.TryGetValue(r.BillNo, out var entered) ? entered : 0d; + r.CargoTareWeight = cargoTareMap.TryGetValue(r.BillNo, out var cargoTare) ? cargoTare : 0d; + ApplyRawMaterialWeight(r); } } + /// 原料重量 = 净重 - 货物皮重(不落库) + private static void ApplyRawMaterialWeight(MesXslWeightRecord record) + { + record.RawMaterialWeight = record.NetWeight.HasValue + ? record.NetWeight.Value - (record.CargoTareWeight ?? 0d) + : null; + } + public async Task GetByIdAsync(string id, CancellationToken ct = default) { MesXslWeightRecord? record = null; @@ -190,7 +203,7 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency if (record != null) { - FillEnteredWeightFromLocalEntries(new List { record }); + FillEntryDerivedFieldsFromLocalEntries(new List { record }); } return record; } @@ -725,8 +738,10 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency GrossWeight = input.GrossWeight, TareWeight = input.TareWeight, NetWeight = input.NetWeight, - // 「已入场重量」由实时聚合写入,Clone 也要原样传递,避免本地缓存 / Pending 重放后被抹掉 + // transient 聚合字段:Clone 原样传递,避免本地缓存 / Pending 重放后被抹掉 EnteredWeight = input.EnteredWeight, + CargoTareWeight = input.CargoTareWeight, + RawMaterialWeight = input.RawMaterialWeight, DriverName = input.DriverName, DriverPhone = input.DriverPhone, BillType = input.BillType, diff --git a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs index 1d88d43..b29175a 100644 --- a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs +++ b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs @@ -162,6 +162,10 @@ public class StompWebSocketService : ISignalRService await SendFrameAsync( BuildSubscribeFrame("sub-mes-warehouse-areas", "/topic/sync/mes-warehouse-areas"), cancellationToken).ConfigureAwait(false); + // 密炼物料皮重策略变更:订阅 /topic/sync/mes-mixer-material-tare-strategies + await SendFrameAsync( + BuildSubscribeFrame("sub-mes-mixer-material-tare-strategies", "/topic/sync/mes-mixer-material-tare-strategies"), + cancellationToken).ConfigureAwait(false); // 打印模板变更:订阅 /topic/sync/print-templates await SendFrameAsync( BuildSubscribeFrame("sub-print-templates", "/topic/sync/print-templates"), diff --git a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs index 121fcbf..0f0b8b9 100644 --- a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs +++ b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs @@ -17,6 +17,7 @@ using YY.Admin.Views.RawMaterialCard; using YY.Admin.Views.WarehouseArea; using YY.Admin.Views.RawMaterialEntry; using YY.Admin.Views.Print; +using YY.Admin.Views.MixerMaterialTareStrategy; namespace YY.Admin { @@ -92,6 +93,8 @@ namespace YY.Admin containerRegistry.RegisterForNavigation(); // 库区管理 containerRegistry.RegisterForNavigation(); + // 密炼物料皮重策略 + containerRegistry.RegisterForNavigation(); // 打印设置 containerRegistry.RegisterForNavigation(); // 打印模板列表 diff --git a/yy-admin-master/YY.Admin/Module/SyncModule.cs b/yy-admin-master/YY.Admin/Module/SyncModule.cs index cc7f558..114b9ce 100644 --- a/yy-admin-master/YY.Admin/Module/SyncModule.cs +++ b/yy-admin-master/YY.Admin/Module/SyncModule.cs @@ -17,6 +17,7 @@ using YY.Admin.Services.Service; using YY.Admin.Services.Service.Category; using YY.Admin.Services.Service.Customer; using YY.Admin.Services.Service.Dict; +using YY.Admin.Services.Service.MixerMaterialTareStrategy; using YY.Admin.Services.Service.MixerMaterial; using YY.Admin.Services.Service.Supplier; using YY.Admin.Services.Service.RawMaterialCard; @@ -69,6 +70,9 @@ public class SyncModule : IModule // 库区管理:免密 API 直连 + STOMP 实时通知 containerRegistry.RegisterSingleton(); containerRegistry.RegisterSingleton(); + // 密炼物料皮重策略:免密 API 直连 + STOMP 实时通知 + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); // 分类字典:启动同步 + 断线重连补刷 containerRegistry.RegisterSingleton(); // 数据字典:启动同步 + 断线重连补刷 @@ -143,6 +147,8 @@ public class SyncModule : IModule _ = containerProvider.Resolve(); // 强制实例化库区同步协调器 _ = containerProvider.Resolve(); + // 强制实例化密炼物料皮重策略同步协调器 + _ = containerProvider.Resolve(); // 强制实例化分类字典同步协调器 _ = containerProvider.Resolve(); // 强制实例化数据字典同步协调器 diff --git a/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs index dcf315a..6877c93 100644 --- a/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs @@ -147,6 +147,11 @@ namespace YY.Admin.ViewModels.Control ["/xslmes/mesXslWarehouseArea"] = "WarehouseAreaListView", ["mesXslWarehouseArea"] = "WarehouseAreaListView", + // 已实现页面:密炼物料皮重策略 + ["MixerMaterialTareStrategyListView"] = "MixerMaterialTareStrategyListView", + ["/xslmes/mesXslMixerMaterialTareStrategy"] = "MixerMaterialTareStrategyListView", + ["mesXslMixerMaterialTareStrategy"] = "MixerMaterialTareStrategyListView", + // 已实现页面:打印设置 ["PrintSettingsView"] = "PrintSettingsView", ["/system/printSettings"] = "PrintSettingsView", diff --git a/yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogViewModel.cs new file mode 100644 index 0000000..d20050f --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogViewModel.cs @@ -0,0 +1,295 @@ +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; +using YY.Admin.Core.Session; +using YY.Admin.ViewModels.RawMaterialEntry; +using YY.Admin.ViewModels.WeightRecord; +using YY.Admin.Views.RawMaterialEntry; +using YY.Admin.Views.WeightRecord; + +namespace YY.Admin.ViewModels.MixerMaterialTareStrategy; + +public class MixerMaterialTareStrategyEditDialogViewModel : BaseViewModel, IDialogResultable +{ + private readonly IMixerMaterialTareStrategyService _tareStrategyService; + + private MesXslMixerMaterialTareStrategy? _strategy; + public MesXslMixerMaterialTareStrategy? Strategy + { + get => _strategy; + set => SetProperty(ref _strategy, value); + } + + public bool IsAddMode => string.IsNullOrWhiteSpace(Strategy?.Id) || (Strategy?.Id?.StartsWith("local-") ?? false); + public string DialogTitle => IsAddMode ? "新增密炼物料皮重策略" : "编辑密炼物料皮重策略"; + + public ObservableCollection UnitOptions { get; } = new(); + + private MesMixerMaterial? _selectedMaterial; + public MesMixerMaterial? SelectedMaterial + { + get => _selectedMaterial; + set + { + SetProperty(ref _selectedMaterial, value); + RaisePropertyChanged(nameof(SelectedMaterialDisplay)); + RaisePropertyChanged(nameof(HasSelectedMaterial)); + if (Strategy != null && value != null) + { + Strategy.MixerMaterialId = value.Id; + Strategy.MixerMaterialName = value.MaterialName; + RaisePropertyChanged(nameof(Strategy)); + } + } + } + + public string SelectedMaterialDisplay => _selectedMaterial != null + ? $"[{_selectedMaterial.MaterialCode}] {_selectedMaterial.MaterialName}" + : (string.IsNullOrWhiteSpace(Strategy?.MixerMaterialName) ? "请选择密炼物料" : Strategy!.MixerMaterialName!); + + public bool HasSelectedMaterial => + _selectedMaterial != null || !string.IsNullOrWhiteSpace(Strategy?.MixerMaterialId); + + private MesXslSupplier? _selectedSupplier; + public MesXslSupplier? SelectedSupplier + { + get => _selectedSupplier; + set + { + SetProperty(ref _selectedSupplier, value); + RaisePropertyChanged(nameof(SelectedSupplierDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + if (Strategy != null && value != null) + { + Strategy.SupplierId = value.Id; + Strategy.SupplierName = value.SupplierName; + RaisePropertyChanged(nameof(Strategy)); + } + } + } + + public string SelectedSupplierDisplay => _selectedSupplier != null + ? $"[{_selectedSupplier.SupplierCode}] {_selectedSupplier.SupplierName}" + : (string.IsNullOrWhiteSpace(Strategy?.SupplierName) ? "请选择供应商" : Strategy!.SupplierName!); + + public bool HasSelectedSupplier => + _selectedSupplier != null || !string.IsNullOrWhiteSpace(Strategy?.SupplierId); + + private bool _result; + public bool Result { get => _result; set => SetProperty(ref _result, value); } + public Action? CloseAction { get; set; } + + public DelegateCommand SaveCommand { get; } + public DelegateCommand CancelCommand { get; } + public DelegateCommand OpenMaterialPickerCommand { get; } + public DelegateCommand ClearMaterialCommand { get; } + public DelegateCommand OpenSupplierPickerCommand { get; } + public DelegateCommand ClearSupplierCommand { get; } + + public MixerMaterialTareStrategyEditDialogViewModel( + IMixerMaterialTareStrategyService tareStrategyService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _tareStrategyService = tareStrategyService; + SaveCommand = new DelegateCommand(async () => await SaveAsync()); + CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); + OpenMaterialPickerCommand = new DelegateCommand(async () => await OpenMaterialPickerAsync()); + ClearMaterialCommand = new DelegateCommand(ClearMaterialSelection); + OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync()); + ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection); + _ = LoadUnitsAsync(); + } + + private async Task LoadUnitsAsync() + { + try + { + var units = await _tareStrategyService.GetUnitsAsync(); + UnitOptions.Clear(); + foreach (var unit in units.Where(u => !string.IsNullOrWhiteSpace(u.Id))) + UnitOptions.Add(unit); + } + catch + { + UnitOptions.Clear(); + } + } + + public void InitializeForAdd() + { + Strategy = new MesXslMixerMaterialTareStrategy(); + _selectedMaterial = null; + _selectedSupplier = null; + RaisePropertyChanged(nameof(IsAddMode)); + RaisePropertyChanged(nameof(DialogTitle)); + RaisePropertyChanged(nameof(SelectedMaterialDisplay)); + RaisePropertyChanged(nameof(SelectedSupplierDisplay)); + RaisePropertyChanged(nameof(HasSelectedMaterial)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + } + + public void InitializeForEdit(MesXslMixerMaterialTareStrategy strategy) + { + Strategy = new MesXslMixerMaterialTareStrategy + { + Id = strategy.Id, + TenantId = strategy.TenantId, + MixerMaterialId = strategy.MixerMaterialId, + MixerMaterialName = strategy.MixerMaterialName, + SupplierId = strategy.SupplierId, + SupplierName = strategy.SupplierName, + MaterialSpec = strategy.MaterialSpec, + TareWeight = strategy.TareWeight, + PalletWeight = strategy.PalletWeight, + UnitId = strategy.UnitId, + UnitName = strategy.UnitName, + EffectiveStartDate = strategy.EffectiveStartDate, + EffectiveEndDate = strategy.EffectiveEndDate, + MaintainBy = strategy.MaintainBy, + UpdateTime = strategy.UpdateTime + }; + _selectedMaterial = null; + _selectedSupplier = null; + RaisePropertyChanged(nameof(IsAddMode)); + RaisePropertyChanged(nameof(DialogTitle)); + RaisePropertyChanged(nameof(SelectedMaterialDisplay)); + RaisePropertyChanged(nameof(SelectedSupplierDisplay)); + RaisePropertyChanged(nameof(HasSelectedMaterial)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + } + + private async Task OpenMaterialPickerAsync() + { + RawMaterialEntryMaterialPickerDialogViewModel? pickerVm = null; + bool confirmed; + try + { + confirmed = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => + { + pickerVm = vm; + vm.Initialize(Strategy?.MixerMaterialId, Strategy?.MixerMaterialName); + }) + .GetResultAsync(); + } + catch { return; } + + if (!confirmed || pickerVm?.SelectedMaterial == null) return; + SelectedMaterial = pickerVm.SelectedMaterial; + } + + private void ClearMaterialSelection() + { + _selectedMaterial = null; + RaisePropertyChanged(nameof(SelectedMaterial)); + RaisePropertyChanged(nameof(SelectedMaterialDisplay)); + RaisePropertyChanged(nameof(HasSelectedMaterial)); + if (Strategy == null) return; + Strategy.MixerMaterialId = null; + Strategy.MixerMaterialName = null; + RaisePropertyChanged(nameof(Strategy)); + } + + private async Task OpenSupplierPickerAsync() + { + SupplierPickerDialogViewModel? pickerVm = null; + bool confirmed; + try + { + confirmed = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => pickerVm = vm) + .GetResultAsync(); + } + catch { return; } + + if (!confirmed || pickerVm?.SelectedSupplier == null) return; + SelectedSupplier = pickerVm.SelectedSupplier; + } + + private void ClearSupplierSelection() + { + _selectedSupplier = null; + RaisePropertyChanged(nameof(SelectedSupplier)); + RaisePropertyChanged(nameof(SelectedSupplierDisplay)); + RaisePropertyChanged(nameof(HasSelectedSupplier)); + if (Strategy == null) return; + Strategy.SupplierId = null; + Strategy.SupplierName = null; + RaisePropertyChanged(nameof(Strategy)); + } + + private async Task SaveAsync() + { + if (Strategy == null) return; + + if (string.IsNullOrWhiteSpace(Strategy.MixerMaterialId)) + { + HandyControl.Controls.MessageBox.Warning("请选择密炼物料!"); + return; + } + if (string.IsNullOrWhiteSpace(Strategy.SupplierId)) + { + HandyControl.Controls.MessageBox.Warning("请选择供应商!"); + return; + } + if (!Strategy.TareWeight.HasValue) + { + HandyControl.Controls.MessageBox.Warning("请填写包装物重量!"); + return; + } + if (Strategy.PalletWeight.HasValue && Strategy.PalletWeight.Value < 0) + { + HandyControl.Controls.MessageBox.Warning("托盘重量不能为负数!"); + return; + } + if (string.IsNullOrWhiteSpace(Strategy.UnitId)) + { + HandyControl.Controls.MessageBox.Warning("请选择单位!"); + return; + } + if (!Strategy.EffectiveStartDate.HasValue || !Strategy.EffectiveEndDate.HasValue) + { + HandyControl.Controls.MessageBox.Warning("请填写完整的生效日期!"); + return; + } + if (Strategy.EffectiveStartDate.Value.Date > Strategy.EffectiveEndDate.Value.Date) + { + HandyControl.Controls.MessageBox.Warning("生效开始日期不能晚于截止日期!"); + return; + } + + var selectedUnit = UnitOptions.FirstOrDefault(u => string.Equals(u.Id, Strategy.UnitId, StringComparison.OrdinalIgnoreCase)); + if (selectedUnit != null) + Strategy.UnitName = selectedUnit.UnitName; + + Strategy.MaintainBy = AppSession.CurrentUser?.Account ?? Strategy.MaintainBy; + Strategy.MaterialSpec = string.IsNullOrWhiteSpace(Strategy.MaterialSpec) + ? null + : Strategy.MaterialSpec.Trim(); + + try + { + bool ok; + if (IsAddMode) + { + ok = await _tareStrategyService.AddAsync(Strategy); + if (ok) HandyControl.Controls.MessageBox.Success("新增成功!"); + else { HandyControl.Controls.MessageBox.Error("新增失败!"); return; } + } + else + { + ok = await _tareStrategyService.EditAsync(Strategy); + if (!ok) { HandyControl.Controls.MessageBox.Error("编辑失败!"); return; } + } + Result = ok; + CloseAction?.Invoke(); + } + catch (Exception ex) + { + HandyControl.Controls.MessageBox.Error($"操作失败:{ex.Message}"); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyListViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyListViewModel.cs new file mode 100644 index 0000000..50ad3c3 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/MixerMaterialTareStrategy/MixerMaterialTareStrategyListViewModel.cs @@ -0,0 +1,221 @@ +using HandyControl.Controls; +using HandyControl.Tools.Extension; +using Prism.Events; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Windows; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Events; +using YY.Admin.Core.Helper; +using YY.Admin.Core.Services; +using YY.Admin.Views.MixerMaterialTareStrategy; + +namespace YY.Admin.ViewModels.MixerMaterialTareStrategy; + +public class MixerMaterialTareStrategyListViewModel : BaseViewModel +{ + private readonly IMixerMaterialTareStrategyService _tareStrategyService; + private SubscriptionToken? _changedToken; + private SubscriptionToken? _syncConflictToken; + + private ObservableCollection _strategies = new(); + public ObservableCollection Strategies + { + get => _strategies; + set => SetProperty(ref _strategies, value); + } + + private long _total; + public long Total { get => _total; set => SetProperty(ref _total, value); } + + private int _pageNo = 1; + public int PageNo { get => _pageNo; set => SetProperty(ref _pageNo, value); } + + private int _pageSize = 20; + public int PageSize { get => _pageSize; set => SetProperty(ref _pageSize, value); } + + private string? _filterMixerMaterialName; + public string? FilterMixerMaterialName + { + get => _filterMixerMaterialName; + set => SetProperty(ref _filterMixerMaterialName, value); + } + + private string? _filterSupplierName; + public string? FilterSupplierName + { + get => _filterSupplierName; + set => SetProperty(ref _filterSupplierName, value); + } + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ResetCommand { get; } + public DelegateCommand AddCommand { get; } + public DelegateCommand EditCommand { get; } + public DelegateCommand DeleteCommand { get; } + public DelegateCommand PrevPageCommand { get; } + public DelegateCommand NextPageCommand { get; } + + public MixerMaterialTareStrategyListViewModel( + IMixerMaterialTareStrategyService tareStrategyService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _tareStrategyService = tareStrategyService; + + SearchCommand = new DelegateCommand(async () => { PageNo = 1; await LoadAsync(); }); + ResetCommand = new DelegateCommand(async () => + { + FilterMixerMaterialName = null; + FilterSupplierName = null; + PageNo = 1; + await LoadAsync(); + }); + AddCommand = new DelegateCommand(async () => await ShowAddDialogAsync()); + EditCommand = new DelegateCommand(async s => await ShowEditDialogAsync(s)); + DeleteCommand = new DelegateCommand(async s => await DeleteAsync(s)); + PrevPageCommand = new DelegateCommand(async () => { if (PageNo > 1) { PageNo--; await LoadAsync(); } }); + NextPageCommand = new DelegateCommand(async () => { if ((long)PageNo * PageSize < Total) { PageNo++; await LoadAsync(); } }); + + _changedToken = _eventAggregator.GetEvent() + .Subscribe(async p => await OnChangedAsync(p), ThreadOption.UIThread); + _syncConflictToken = _eventAggregator.GetEvent() + .Subscribe(OnSyncConflict, ThreadOption.UIThread); + + _ = InitializeAsync(); + } + + private async Task OnChangedAsync(MixerMaterialTareStrategyChangedPayload payload) + { + if (payload.Action == "edit" && !string.IsNullOrWhiteSpace(payload.TareStrategyId)) + await RefreshSingleAsync(payload.TareStrategyId!); + else + await LoadAsync(); + } + + private async Task RefreshSingleAsync(string id) + { + try + { + var updated = await _tareStrategyService.GetByIdAsync(id); + if (updated == null) return; + var idx = Strategies.ToList().FindIndex(s => string.Equals(s.Id, id, StringComparison.OrdinalIgnoreCase)); + if (idx >= 0) Strategies[idx] = updated; + } + catch (Exception ex) + { + Debug.WriteLine($"[密炼物料皮重策略] 单条刷新失败: {ex.Message}"); + } + } + + private void OnSyncConflict(SyncConflictPayload payload) + { + if (!string.Equals(payload.EntityName, "密炼物料皮重策略", StringComparison.OrdinalIgnoreCase)) return; + + var parts = new List(); + if (payload.PushedCount > 0) + parts.Add($"已同步 {payload.PushedCount} 条本地改动到服务器"); + if (payload.NewRecordsPushed > 0) + parts.Add($"已上传 {payload.NewRecordsPushed} 条本地新增记录"); + if (payload.ConflictCount > 0) + parts.Add($"{payload.ConflictCount} 条记录与服务器版本冲突,已保留服务器版本"); + + if (parts.Count == 0) return; + var message = string.Join("\n", parts); + if (payload.ConflictCount > 0) Growl.Warning(message); + else Growl.Success(message); + } + + private async Task InitializeAsync() + { + try + { + await UIHelper.WaitForRenderAsync(); + await LoadAsync(); + } + catch (Exception ex) + { + Debug.WriteLine($"密炼物料皮重策略列表初始化失败: {ex.Message}"); + } + } + + public async Task LoadAsync() + { + try + { + IsLoading = true; + var result = await _tareStrategyService.PageAsync( + PageNo, PageSize, FilterMixerMaterialName, FilterSupplierName); + Strategies = new ObservableCollection(result.Records); + Total = result.Total; + } + catch (Exception ex) + { + Growl.Error($"加载密炼物料皮重策略失败:{ex.Message}"); + } + finally + { + IsLoading = false; + } + } + + private async Task ShowAddDialogAsync() + { + try + { + var result = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => vm.InitializeForAdd()) + .GetResultAsync(); + if (result) await LoadAsync(); + } + catch (Exception ex) + { + Growl.Error($"打开新增对话框失败:{ex.Message}"); + } + } + + private async Task ShowEditDialogAsync(MesXslMixerMaterialTareStrategy strategy) + { + if (strategy == null) return; + try + { + var result = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => vm.InitializeForEdit(strategy)) + .GetResultAsync(); + if (result) await LoadAsync(); + } + catch (Exception ex) + { + Growl.Error($"打开编辑对话框失败:{ex.Message}"); + } + } + + private async Task DeleteAsync(MesXslMixerMaterialTareStrategy strategy) + { + if (strategy?.Id == null) return; + var confirm = System.Windows.MessageBox.Show( + $"确定删除【{strategy.MixerMaterialName} / {strategy.SupplierName}】的包装物重量策略?此操作不可恢复!", + "确认删除", MessageBoxButton.OKCancel, MessageBoxImage.Question); + if (confirm != System.Windows.MessageBoxResult.OK) return; + + var ok = await _tareStrategyService.DeleteAsync(strategy.Id); + if (ok) { Growl.Success("删除成功!"); await LoadAsync(); } + else Growl.Error("删除失败!"); + } + + protected override void CleanUp() + { + base.CleanUp(); + if (_changedToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_changedToken); + _changedToken = null; + } + if (_syncConflictToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_syncConflictToken); + _syncConflictToken = null; + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardEditDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardEditDialogViewModel.cs index 11c5d49..c5e1d8d 100644 --- a/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardEditDialogViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialCard/RawMaterialCardEditDialogViewModel.cs @@ -112,6 +112,8 @@ public class RawMaterialCardEditDialogViewModel : BaseViewModel, IDialogResultab ManufacturerMaterialName = card.ManufacturerMaterialName, ShelfLife = card.ShelfLife, TotalWeight = card.TotalWeight, + PackagingTare = card.PackagingTare, + PalletWeight = card.PalletWeight, RemainingWeight = card.RemainingWeight, RemainingQuantity = card.RemainingQuantity, Status = card.Status, diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs index cb52ef2..d278e75 100644 --- a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryEditDialogViewModel.cs @@ -9,6 +9,7 @@ using YY.Admin.Core; using YY.Admin.Core.Entity; using YY.Admin.Core.Services; using YY.Admin.Services.Service; +using YY.Admin.Services.Service.MixerMaterialTareStrategy; using YY.Admin.Views.RawMaterialEntry; using YY.Admin.ViewModels.WeightRecord; using YY.Admin.Views.WeightRecord; @@ -22,8 +23,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta protected IRawMaterialEntryService EntryService => _entryService; private readonly IJeecgDictSyncService _dictSyncService; private readonly IMixerMaterialService _mixerMaterialService; - - // 加载完物料后用于回填 Edit 模式选中项 + private readonly IMixerMaterialTareStrategyService _tareStrategyService; + private readonly ISupplierService _supplierService; + private bool _suppressTareStrategyRefresh; protected string? _pendingMaterialId; private MesXslRawMaterialEntry? _entry; @@ -49,6 +51,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta RaisePropertyChanged(nameof(SelectedMaterialDisplay)); RaisePropertyChanged(nameof(HasSelectedMaterial)); + _ = RefreshAllRowsTareStrategyAsync(); + // 新增模式自动生成条码/批次号 if (IsAddMode && !string.IsNullOrEmpty(value.MaterialCode)) _ = AutoGenerateBarcodeAsync(value.MaterialCode); @@ -106,6 +110,27 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta } } + public DateTime? EntryTimeInput + { + get => Entry?.EntryTime; + set + { + if (Entry == null || Entry.EntryTime == value) return; + Entry.EntryTime = value; + RaisePropertyChanged(nameof(Entry)); + _ = RefreshAllRowsTareStrategyAsync(); + } + } + + /// 基础资料「托盘及皮重(合计)」= Σ 份数 × (包装物皮重 + 托盘重量)。 + public double PalletTareTotalDisplay => SplitCodeDetails.Sum(row => + { + var portions = row.Portions ?? 0; + var packaging = row.PackagingTare ?? 0d; + var pallet = row.PalletWeight ?? 0d; + return portions * (packaging + pallet); + }); + public ObservableCollection MaterialOptions { get; } = new(); public ObservableCollection> TestResultOptions { get; } = new(); public ObservableCollection> TestStatusOptions { get; } = new(); @@ -174,21 +199,27 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta public DelegateCommand ClearWeightRecordCommand { get; } public DelegateCommand OpenSupplierPickerCommand { get; } public DelegateCommand ClearSupplierCommand { get; } - /// - /// 拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。 - /// + /// 拆码明细 - 库位选择命令(弹出「库区选择」弹窗,单选)。CommandParameter 为当前行 RawMaterialSplitDetailItem。 public DelegateCommand OpenWarehouseAreaPickerCommand { get; } + /// 拆码明细 - 手动选择皮重策略。 + public DelegateCommand OpenTareStrategyPickerCommand { get; } + /// 拆码明细 - 清除手动策略并按规则重新自动匹配。 + public DelegateCommand ResetTareStrategyCommand { get; } public RawMaterialEntryEditDialogViewModel( IRawMaterialEntryService entryService, IJeecgDictSyncService dictSyncService, IMixerMaterialService mixerMaterialService, + IMixerMaterialTareStrategyService tareStrategyService, + ISupplierService supplierService, IContainerExtension container, IRegionManager regionManager) : base(container, regionManager) { _entryService = entryService; _dictSyncService = dictSyncService; _mixerMaterialService = mixerMaterialService; + _tareStrategyService = tareStrategyService; + _supplierService = supplierService; SaveCommand = new DelegateCommand(async () => await SaveAsync()); CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); ResetCommand = new DelegateCommand(InitializeForAdd); @@ -201,6 +232,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync()); ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection); OpenWarehouseAreaPickerCommand = new DelegateCommand(async row => await OpenWarehouseAreaPickerAsync(row)); + OpenTareStrategyPickerCommand = new DelegateCommand(async row => await OpenTareStrategyPickerAsync(row)); + ResetTareStrategyCommand = new DelegateCommand(async row => await ResetTareStrategyAsync(row)); SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged; _ = LoadAllAsync(); } @@ -349,6 +382,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta RaisePropertyChanged(nameof(IsAddMode)); RaisePropertyChanged(nameof(DialogTitle)); RaisePropertyChanged(nameof(TotalWeightInput)); + RaisePropertyChanged(nameof(EntryTimeInput)); + RaisePropertyChanged(nameof(PalletTareTotalDisplay)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); @@ -367,8 +402,13 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta MaterialId = entry.MaterialId, MaterialCode = entry.MaterialCode, MaterialName = entry.MaterialName, SupplyCustomer = entry.SupplyCustomer, SupplierId = entry.SupplierId, SupplierName = entry.SupplierName, ManufacturerMaterialName = entry.ManufacturerMaterialName, - ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, TotalPortions = entry.TotalPortions, - PortionWeight = entry.PortionWeight, PortionPackages = entry.PortionPackages, + ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, PalletTareTotal = entry.PalletTareTotal, + TotalPortions = entry.TotalPortions, + PortionWeight = entry.PortionWeight, + PortionPackagingTare = entry.PortionPackagingTare, + PortionPalletWeight = entry.PortionPalletWeight, + PortionTareStrategyIds = entry.PortionTareStrategyIds, + PortionPackages = entry.PortionPackages, PortionWarehouseLocations = entry.PortionWarehouseLocations, PortionDetailIds = entry.PortionDetailIds, PortionCardFlags = entry.PortionCardFlags, @@ -398,6 +438,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta RaisePropertyChanged(nameof(IsAddMode)); RaisePropertyChanged(nameof(DialogTitle)); RaisePropertyChanged(nameof(TotalWeightInput)); + RaisePropertyChanged(nameof(EntryTimeInput)); + RaisePropertyChanged(nameof(PalletTareTotalDisplay)); RaisePropertyChanged(nameof(IsSpecialAdoptionValue)); RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); @@ -503,6 +545,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta Entry.BillNo = selected.BillNo; Entry.SupplierName = selected.SenderUnit; Entry.SupplierId = null; + await ApplySupplierFromDisplayNameAsync(selected.SenderUnit); // 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」, // 用户仍可手动编辑;若净重为空则保留原值,避免误清空。 if (selected.NetWeight.HasValue) @@ -514,6 +557,76 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta } RaisePropertyChanged(nameof(Entry)); RaisePropertyChanged(nameof(TotalWeightInput)); + RaisePropertyChanged(nameof(EntryTimeInput)); + RaisePropertyChanged(nameof(PalletTareTotalDisplay)); + await RefreshAllRowsTareStrategyAsync(); + } + + /// + /// 按供应商全称/简称反查 ID(榜单「发货单位」与手动填写的供应商名称均可用)。 + /// + private async Task ApplySupplierFromDisplayNameAsync(string? displayName) + { + if (Entry == null || string.IsNullOrWhiteSpace(displayName)) + { + return; + } + + var supplier = await FindSupplierByDisplayNameAsync(displayName); + if (supplier == null) + { + return; + } + + Entry.SupplierId = supplier.Id; + Entry.SupplierName = supplier.SupplierName + ?? supplier.SupplierShortName + ?? displayName.Trim(); + RaisePropertyChanged(nameof(Entry)); + } + + private async Task FindSupplierByDisplayNameAsync(string? displayName) + { + if (string.IsNullOrWhiteSpace(displayName)) + { + return null; + } + + var name = displayName.Trim(); + try + { + var page = await _supplierService.PageAsync(1, 5000); + var exact = page.Records.FirstOrDefault(s => + string.Equals(s.SupplierName?.Trim(), name, StringComparison.OrdinalIgnoreCase) + || string.Equals(s.SupplierShortName?.Trim(), name, StringComparison.OrdinalIgnoreCase)); + if (exact != null) + { + return exact; + } + + return page.Records.FirstOrDefault(s => + (s.SupplierName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase) + || (s.SupplierShortName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase) + || name.Contains(s.SupplierShortName ?? "", StringComparison.OrdinalIgnoreCase) + || name.Contains(s.SupplierName ?? "", StringComparison.OrdinalIgnoreCase)); + } + catch + { + return null; + } + } + + /// + /// 皮重策略匹配前确保 SupplierId 已解析(榜单仅带出名称时补全 ID)。 + /// + private async Task EnsureSupplierIdForTareMatchAsync() + { + if (Entry == null || !string.IsNullOrWhiteSpace(Entry.SupplierId)) + { + return; + } + + await ApplySupplierFromDisplayNameAsync(Entry.SupplierName); } /// @@ -617,6 +730,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta Entry.SupplierId = null; Entry.SupplierName = null; RaisePropertyChanged(nameof(Entry)); + _ = RefreshAllRowsTareStrategyAsync(); } private async Task OpenSupplierPickerAsync() @@ -638,6 +752,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta Entry.SupplierId = selected.Id; Entry.SupplierName = selected.SupplierName; RaisePropertyChanged(nameof(Entry)); + await RefreshAllRowsTareStrategyAsync(); } private void ClearSupplierSelection() @@ -646,8 +761,128 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta Entry.SupplierId = null; Entry.SupplierName = null; RaisePropertyChanged(nameof(Entry)); + _ = RefreshAllRowsTareStrategyAsync(); } + private async Task RefreshAllRowsTareStrategyAsync() + { + if (_suppressTareStrategyRefresh) return; + await EnsureSupplierIdForTareMatchAsync(); + foreach (var row in SplitCodeDetails.ToList()) + { + if (!row.IsManualTareStrategy) + await ApplyAutoTareStrategyToRowAsync(row); + } + } + + private async Task ApplyAutoTareStrategyToRowAsync(RawMaterialSplitDetailItem row) + { + if (_suppressTareStrategyRefresh || row.IsManualTareStrategy || row.HasCard) return; + + try + { + var strategies = await _tareStrategyService.GetAllForMatchAsync(); + var match = MixerMaterialTareStrategyMatcher.PickBestMatch( + strategies, + Entry?.MaterialId, + Entry?.SupplierId, + Entry?.EntryTime, + row.PortionWeight); + ApplyTareStrategyToRow(row, match, manual: false); + } + catch + { + ApplyTareStrategyToRow(row, null, manual: false); + } + + RaisePalletTareTotalChanged(); + } + + private void ApplyTareStrategyToRow( + RawMaterialSplitDetailItem row, + MesXslMixerMaterialTareStrategy? strategy, + bool manual) + { + if (strategy == null) + { + row.PackagingTare = 0d; + row.PalletWeight = 0d; + row.TareStrategyId = null; + row.TareStrategyDisplay = "未匹配(0)"; + } + else + { + row.PackagingTare = strategy.TareWeight.HasValue ? (double)strategy.TareWeight.Value : 0d; + row.PalletWeight = strategy.PalletWeight.HasValue ? (double)strategy.PalletWeight.Value : 0d; + row.TareStrategyId = strategy.Id; + row.TareStrategyDisplay = BuildTareStrategyDisplay(strategy); + } + + row.IsManualTareStrategy = manual && strategy != null; + } + + private static string BuildTareStrategyDisplay(MesXslMixerMaterialTareStrategy strategy) + { + var spec = string.IsNullOrWhiteSpace(strategy.MaterialSpec) ? "-" : strategy.MaterialSpec; + var pkg = strategy.TareWeight?.ToString("0.##") ?? "0"; + var pallet = strategy.PalletWeight?.ToString("0.##") ?? "0"; + return $"规格:{spec} 包装:{pkg} 托盘:{pallet}"; + } + + private async Task OpenTareStrategyPickerAsync(RawMaterialSplitDetailItem? row) + { + if (row == null || row.HasCard) return; + if (string.IsNullOrWhiteSpace(Entry?.MaterialId)) + { + HandyControl.Controls.MessageBox.Warning("请先选择密炼物料!"); + return; + } + + await EnsureSupplierIdForTareMatchAsync(); + if (string.IsNullOrWhiteSpace(Entry?.SupplierId)) + { + HandyControl.Controls.MessageBox.Warning("未能匹配到供应商档案,请手动选择供应商,或检查榜单发货单位是否与供应商简称/全称一致!"); + return; + } + + TareStrategyPickerDialogViewModel? pickerVm = null; + bool confirmed; + try + { + confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(async () => + { + return await HandyControl.Controls.Dialog.Show() + .Initialize(vm => + { + pickerVm = vm; + vm.Initialize(Entry!.MaterialId, Entry.SupplierId, Entry.EntryTime, row.PortionWeight, row.TareStrategyId); + }) + .GetResultAsync(); + }); + } + catch { return; } + + if (!confirmed) return; + if (pickerVm?.SelectedStrategy == null) + { + row.IsManualTareStrategy = false; + await ApplyAutoTareStrategyToRowAsync(row); + return; + } + + ApplyTareStrategyToRow(row, pickerVm.SelectedStrategy, manual: true); + RaisePalletTareTotalChanged(); + } + + private async Task ResetTareStrategyAsync(RawMaterialSplitDetailItem? row) + { + if (row == null || row.HasCard) return; + row.IsManualTareStrategy = false; + await ApplyAutoTareStrategyToRowAsync(row); + } + + private void RaisePalletTareTotalChanged() => RaisePropertyChanged(nameof(PalletTareTotalDisplay)); + private void RecalculateShelfLife(MesMixerMaterial? material) { if (Entry == null || material?.ShelfLifeDays == null || material.ShelfLifeDays <= 0) @@ -719,63 +954,68 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta protected void InitializeSplitCodeDetailsFromEntry() { - SplitCodeDetails.Clear(); - - // 六个字段都是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表 - var portionsArr = SplitJoinedValues(Entry?.TotalPortions); - var weightArr = SplitJoinedValues(Entry?.PortionWeight); - var packagesArr = SplitJoinedValues(Entry?.PortionPackages); - var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations); - var idsArr = SplitJoinedValues(Entry?.PortionDetailIds); - var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags); - // 历史记录可能没存 PortionCardFlags:用 print_flag 推断(与原行为兼容) - var fallbackToPrintFlag = cardFlagsArr.Length == 0 - && string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal); - var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length), - Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length))); - - for (var i = 0; i < rowCount; i++) + _suppressTareStrategyRefresh = true; + try { - var locationFromArr = GetAt(locationsArr, i); - // 兼容历史数据:明细库位拼接字段为空时,首行回退到 Entry.WarehouseLocation - // (早期版本里整票级库位曾被反写过,避免老记录打开后明细全空) - var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr)) - ? Entry?.WarehouseLocation - : locationFromArr; + SplitCodeDetails.Clear(); - var item = new RawMaterialSplitDetailItem + var portionsArr = SplitJoinedValues(Entry?.TotalPortions); + var weightArr = SplitJoinedValues(Entry?.PortionWeight); + var packagingTareArr = SplitJoinedValues(Entry?.PortionPackagingTare); + var palletWeightArr = SplitJoinedValues(Entry?.PortionPalletWeight); + var strategyIdsArr = SplitJoinedValues(Entry?.PortionTareStrategyIds); + var packagesArr = SplitJoinedValues(Entry?.PortionPackages); + var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations); + var idsArr = SplitJoinedValues(Entry?.PortionDetailIds); + var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags); + var fallbackToPrintFlag = cardFlagsArr.Length == 0 + && string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal); + var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length), + Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length))); + + for (var i = 0; i < rowCount; i++) { - Portions = TryParseInt(GetAt(portionsArr, i)), - PortionWeight = TryParseDouble(GetAt(weightArr, i)), - PortionPackages = TryParseInt(GetAt(packagesArr, i)), - WarehouseLocation = locationFallback, - }; - // 仅当后端已持久化了该行 ID 时才覆盖默认值,确保跨次保持稳定; - // 历史记录无 ID 字段时使用构造期生成的新 GUID(之后保存会回填)。 - var existingId = GetAt(idsArr, i); - if (!string.IsNullOrWhiteSpace(existingId)) - { - item.Id = existingId; + var locationFromArr = GetAt(locationsArr, i); + var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr)) + ? Entry?.WarehouseLocation + : locationFromArr; + + var strategyId = GetAt(strategyIdsArr, i); + var item = new RawMaterialSplitDetailItem + { + Portions = TryParseInt(GetAt(portionsArr, i)), + PortionWeight = TryParseDouble(GetAt(weightArr, i)), + PortionPackages = TryParseInt(GetAt(packagesArr, i)), + WarehouseLocation = locationFallback, + PackagingTare = TryParseDouble(GetAt(packagingTareArr, i)) ?? 0d, + PalletWeight = TryParseDouble(GetAt(palletWeightArr, i)) ?? 0d, + TareStrategyId = strategyId, + IsManualTareStrategy = !string.IsNullOrWhiteSpace(strategyId), + }; + item.TareStrategyDisplay = string.IsNullOrWhiteSpace(strategyId) + ? "未匹配(0)" + : $"策略ID:{strategyId}"; + + var existingId = GetAt(idsArr, i); + if (!string.IsNullOrWhiteSpace(existingId)) + item.Id = existingId; + + var flagAt = GetAt(cardFlagsArr, i); + if (!string.IsNullOrWhiteSpace(flagAt)) + item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal); + else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId)) + item.HasCard = true; + + SplitCodeDetails.Add(item); } - // HasCard 行级解析(优先级 1):从 PortionCardFlags 按位读取,"1"=已生成。 - // 这是「保存后新增未生成行不被误判为已打印」的关键 — 新增行保存时 HasCard=false 持久化为 "0", - // 重新加载时回填 false,「生成原材料卡片」就能正确把它列入待生成清单。 - var flagAt = GetAt(cardFlagsArr, i); - if (!string.IsNullOrWhiteSpace(flagAt)) - { - item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal); - } - else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId)) - { - // 优先级 2(兼容历史记录):未存 PortionCardFlags 时,沿用旧逻辑—— - // 持久化 ID 存在 + 整票已打印 ⇒ 视为已生成卡片。 - item.HasCard = true; - } - // 否则保持构造默认值 false(新增态、未打印整票等场景) - SplitCodeDetails.Add(item); + + RaisePropertyChanged(nameof(SplitCodeTableHeight)); + RaisePalletTareTotalChanged(); + } + finally + { + _suppressTareStrategyRefresh = false; } - - RaisePropertyChanged(nameof(SplitCodeTableHeight)); } private static string[] SplitJoinedValues(string? value) @@ -798,8 +1038,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta private void AddSplitDetailRow() { - SplitCodeDetails.Add(new RawMaterialSplitDetailItem()); + var row = new RawMaterialSplitDetailItem(); + SplitCodeDetails.Add(row); RaisePropertyChanged(nameof(SplitCodeTableHeight)); + _ = ApplyAutoTareStrategyToRowAsync(row); } private void RemoveSplitDetailRow(RawMaterialSplitDetailItem? item) @@ -841,32 +1083,52 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e) { - // 用户填写或修改某行的「份数」时,按公式自动算出该行「每份重量」: - // 每份重量 = (总重 - Σ其他行的份数×每份重量) / 当前行份数 + // 用户修改某行的「份数」时:仅当该行「每份重量」为空才按公式自动计算 if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions) && sender is RawMaterialSplitDetailItem row) { RecalculatePortionWeightForRow(row); } + if (e.PropertyName == nameof(RawMaterialSplitDetailItem.PortionWeight) + && sender is RawMaterialSplitDetailItem weightRow) + { + // 清空每份重量后,若份数已填则立即按公式重算(配合 PortionWeightText 空值绑定) + if (!weightRow.PortionWeight.HasValue) + { + RecalculatePortionWeightForRow(weightRow); + } + + if (!weightRow.IsManualTareStrategy && !weightRow.HasCard) + { + _ = ApplyAutoTareStrategyToRowAsync(weightRow); + } + } + if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions) or nameof(RawMaterialSplitDetailItem.PortionWeight) - or nameof(RawMaterialSplitDetailItem.PortionPackages)) + or nameof(RawMaterialSplitDetailItem.PortionPackages) + or nameof(RawMaterialSplitDetailItem.PackagingTare) + or nameof(RawMaterialSplitDetailItem.PalletWeight)) { RaiseSplitDisplayPropertyChanged(); } } /// - /// 拆码明细某行的「份数」变化时,按公式重算该行「每份重量」: + /// 拆码明细某行的「份数」变化时,仅当该行「每份重量」为空时按公式重算: /// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数 - /// — 用户单独手改「每份重量」不触发; + /// — 已填写「每份重量」后再改份数,不覆盖用户输入; /// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过; - /// — 结果四舍五入到两位小数; - /// — 写入 row.PortionWeight 会触发 PropertyChanged(PortionWeight),不会再次进入本方法(PropertyName 已不是 Portions),不会形成循环。 + /// — 结果四舍五入到两位小数。 /// private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row) { + if (row.PortionWeight.HasValue) + { + return; + } + if (Entry?.TotalWeight is not { } total || total <= 0d) { return; @@ -898,6 +1160,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta RaisePropertyChanged(nameof(SplitTotalPortionsDisplay)); RaisePropertyChanged(nameof(SplitPortionWeightDisplay)); RaisePropertyChanged(nameof(SplitPortionPackagesDisplay)); + RaisePalletTareTotalChanged(); } private string JoinSplitValue(Func selector, bool withTrailingSlash) @@ -938,6 +1201,11 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta Entry.TotalPortions = JoinSplitValue(it => it.Portions?.ToString(CultureInfo.InvariantCulture), true); Entry.PortionWeight = JoinSplitValue(it => FormatNullableDecimal(it.PortionWeight), true); + Entry.PortionPackagingTare = JoinSplitValue(it => FormatNullableDecimal(it.PackagingTare), true); + Entry.PortionPalletWeight = JoinSplitValue(it => FormatNullableDecimal(it.PalletWeight), true); + Entry.PortionTareStrategyIds = JoinSplitValue( + it => it.IsManualTareStrategy && !string.IsNullOrWhiteSpace(it.TareStrategyId) ? it.TareStrategyId : null, + true); Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true); Entry.PortionWarehouseLocations = JoinSplitValue(it => string.IsNullOrWhiteSpace(it.WarehouseLocation) ? null : it.WarehouseLocation.Trim(), true); // 持久化每行 GUID,行序与上方四个字段对齐;JoinSplitValue 会过滤空,但 Id 在构造时即生成不会为空。 @@ -947,6 +1215,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta // 「生成原材料卡片」只对 HasCard=false 的行加卡 → 必须把真实的 HasCard 持久化, // 否则重新加载后无法区分「已生成」与「保存后新增的未生成行」。 Entry.PortionCardFlags = JoinSplitValue(it => it.HasCard ? "1" : "0", true); + Entry.PalletTareTotal = PalletTareTotalDisplay; } private double CalculateSplitCodeTableHeight() @@ -983,7 +1252,37 @@ public class RawMaterialSplitDetailItem : BindableBase public double? PortionWeight { get => _portionWeight; - set => SetProperty(ref _portionWeight, value); + set + { + if (SetProperty(ref _portionWeight, value)) + { + RaisePropertyChanged(nameof(PortionWeightText)); + } + } + } + + /// + /// 每份重量文本(供 TextBox 绑定;空字符串表示未填写,避免 double? 直接绑定无法清空)。 + /// + public string PortionWeightText + { + get => _portionWeight.HasValue + ? _portionWeight.Value.ToString("0.##", CultureInfo.InvariantCulture) + : string.Empty; + set + { + var text = value?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(text)) + { + PortionWeight = null; + return; + } + + if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) + { + PortionWeight = parsed; + } + } } private int? _portionPackages; @@ -1000,6 +1299,42 @@ public class RawMaterialSplitDetailItem : BindableBase set => SetProperty(ref _warehouseLocation, value); } + private double? _packagingTare; + public double? PackagingTare + { + get => _packagingTare; + set => SetProperty(ref _packagingTare, value); + } + + private double? _palletWeight; + public double? PalletWeight + { + get => _palletWeight; + set => SetProperty(ref _palletWeight, value); + } + + private string? _tareStrategyId; + public string? TareStrategyId + { + get => _tareStrategyId; + set => SetProperty(ref _tareStrategyId, value); + } + + private string? _tareStrategyDisplay = "未匹配(0)"; + public string? TareStrategyDisplay + { + get => _tareStrategyDisplay; + set => SetProperty(ref _tareStrategyDisplay, value); + } + + private bool _isManualTareStrategy; + /// 用户手动选择皮重策略后为 true,自动匹配不再覆盖。 + public bool IsManualTareStrategy + { + get => _isManualTareStrategy; + set => SetProperty(ref _isManualTareStrategy, value); + } + /// /// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。 /// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断; diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs index 103ecb3..4347680 100644 --- a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs +++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/RawMaterialEntryOperationViewModel.cs @@ -242,13 +242,15 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView IRawMaterialEntryService entryService, IJeecgDictSyncService dictSyncService, IMixerMaterialService mixerMaterialService, + IMixerMaterialTareStrategyService tareStrategyService, + ISupplierService supplierService, IRawMaterialCardService rawMaterialCardService, IPrintDotService printDotService, IPrintBizTemplateBindService printBizTemplateBindService, IPrintTemplateService printTemplateService, IContainerExtension container, IRegionManager regionManager) - : base(entryService, dictSyncService, mixerMaterialService, container, regionManager) + : base(entryService, dictSyncService, mixerMaterialService, tareStrategyService, supplierService, container, regionManager) { _rawMaterialCardService = rawMaterialCardService; _printDotService = printDotService; @@ -802,6 +804,8 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView ManufacturerMaterialName = Entry.ManufacturerMaterialName, ShelfLife = Entry.ShelfLife, TotalWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null, + PackagingTare = detail.PackagingTare.HasValue ? (decimal?)detail.PackagingTare.Value : null, + PalletWeight = detail.PalletWeight.HasValue ? (decimal?)detail.PalletWeight.Value : null, RemainingWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null, RemainingQuantity = detail.PortionPackages, WarehouseArea = detail.WarehouseLocation, @@ -845,9 +849,10 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView RaisePropertyChanged(nameof(CanGenerateCards)); RaisePropertyChanged(nameof(CanResplit)); + string? printError = null; if (newCardCount > 0 && generatedCards.Count > 0) { - var printError = await PrintGeneratedRawMaterialCardsAsync(selectedPrinterName, generatedCards); + printError = await PrintGeneratedRawMaterialCardsAsync(selectedPrinterName, generatedCards); if (!string.IsNullOrWhiteSpace(printError)) { HandyControl.Controls.MessageBox.Warning($"卡片已生成 {newCardCount} 张,但打印存在异常:{printError}"); @@ -855,7 +860,12 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView } if (failCount == 0) - HandyControl.Controls.MessageBox.Success($"已生成并打印 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新为「已打印」!"); + { + if (string.IsNullOrWhiteSpace(printError)) + HandyControl.Controls.MessageBox.Success($"已生成并打印 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新为「已打印」!"); + else + HandyControl.Controls.MessageBox.Success($"已生成 {newCardCount} 张原材料卡片(累计 {alreadyGenerated + newCardCount} 张),打印状态已更新;请检查打印机后补打标签。"); + } else HandyControl.Controls.MessageBox.Warning($"本次共尝试生成 {newCardCount + failCount} 张,成功 {newCardCount} 张,失败 {failCount} 张。失败的行未标记为「已打印」,可检查网络后再次点击「生成原材料卡片」重试。"); } @@ -950,6 +960,8 @@ public class RawMaterialEntryOperationViewModel : RawMaterialEntryEditDialogView ManufacturerMaterialName = entry.ManufacturerMaterialName, ShelfLife = entry.ShelfLife, TotalWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null, + PackagingTare = detail.PackagingTare.HasValue ? (decimal?)detail.PackagingTare.Value : null, + PalletWeight = detail.PalletWeight.HasValue ? (decimal?)detail.PalletWeight.Value : null, RemainingWeight = detail.PortionWeight.HasValue ? (decimal?)detail.PortionWeight.Value : null, RemainingQuantity = detail.PortionPackages, WarehouseArea = detail.WarehouseLocation, diff --git a/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/TareStrategyPickerDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/TareStrategyPickerDialogViewModel.cs new file mode 100644 index 0000000..e2e8b65 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/RawMaterialEntry/TareStrategyPickerDialogViewModel.cs @@ -0,0 +1,110 @@ +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.Core.Entity; +using YY.Admin.Core.Services; +using YY.Admin.Services.Service.MixerMaterialTareStrategy; + +namespace YY.Admin.ViewModels.RawMaterialEntry; + +/// +/// 拆码明细手动选择「密炼物料皮重策略」弹窗。 +/// +public class TareStrategyPickerDialogViewModel : BaseViewModel, IDialogResultable +{ + private readonly IMixerMaterialTareStrategyService _tareStrategyService; + + private string? _materialId; + private string? _supplierId; + private DateTime? _entryDate; + private double? _portionWeight; + + public ObservableCollection Records { get; } = new(); + + private MesXslMixerMaterialTareStrategy? _selectedStrategy; + public MesXslMixerMaterialTareStrategy? SelectedStrategy + { + get => _selectedStrategy; + set + { + SetProperty(ref _selectedStrategy, value); + ConfirmCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(SelectedStrategyDisplay)); + } + } + + public string SelectedStrategyDisplay => _selectedStrategy == null + ? "请选中一条策略后确认,或点「使用自动匹配」" + : $"规格:{_selectedStrategy.MaterialSpec ?? "-"} 包装物:{_selectedStrategy.TareWeight:0.###} 托盘:{_selectedStrategy.PalletWeight:0.###}"; + + private bool _result; + public bool Result { get => _result; set => SetProperty(ref _result, value); } + public Action? CloseAction { get; set; } + + public DelegateCommand ConfirmCommand { get; } + public DelegateCommand UseAutoMatchCommand { get; } + public DelegateCommand CancelCommand { get; } + + public TareStrategyPickerDialogViewModel( + IMixerMaterialTareStrategyService tareStrategyService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _tareStrategyService = tareStrategyService; + ConfirmCommand = new DelegateCommand(ConfirmSelection, () => SelectedStrategy != null); + UseAutoMatchCommand = new DelegateCommand(UseAutoMatch); + CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); + } + + public void Initialize( + string? materialId, + string? supplierId, + DateTime? entryDate, + double? portionWeight, + string? currentStrategyId) + { + _materialId = materialId; + _supplierId = supplierId; + _entryDate = entryDate; + _portionWeight = portionWeight; + _ = LoadAsync(currentStrategyId); + } + + private async Task LoadAsync(string? currentStrategyId) + { + try + { + var all = await _tareStrategyService.GetAllForMatchAsync(); + var filtered = MixerMaterialTareStrategyMatcher.FilterCandidates( + all, _materialId, _supplierId, _entryDate, _portionWeight); + + Records.Clear(); + foreach (var item in filtered) + Records.Add(item); + + if (!string.IsNullOrWhiteSpace(currentStrategyId)) + { + SelectedStrategy = Records.FirstOrDefault(r => + string.Equals(r.Id, currentStrategyId, StringComparison.OrdinalIgnoreCase)) + ?? all.FirstOrDefault(r => string.Equals(r.Id, currentStrategyId, StringComparison.OrdinalIgnoreCase)); + } + } + catch + { + Records.Clear(); + } + } + + private void ConfirmSelection() + { + Result = true; + CloseAction?.Invoke(); + } + + private void UseAutoMatch() + { + SelectedStrategy = null; + Result = true; + CloseAction?.Invoke(); + } +} diff --git a/yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml b/yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml new file mode 100644 index 0000000..3c68cd4 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/MixerMaterialTareStrategy/MixerMaterialTareStrategyEditDialogView.xaml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -873,7 +953,7 @@ - +