Merge branch 'main' into 生产及设备基础资料

This commit is contained in:
2026-06-02 16:40:56 +08:00
76 changed files with 4506 additions and 201 deletions

View File

@@ -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)
}

View File

@@ -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");
// 打印模板免密接口(供桌面端调用)

View File

@@ -522,6 +522,71 @@ 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
-- author:jiangxh---date:20250602--for: 【MES】停机记录建表+菜单+CRUD+列表录入维修结果弹窗 ---
jeecg-boot/db/mes-xsl-downtime-record-menu-permission.sql
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_downtime_record.sql

View File

@@ -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<MesXslWeightRecord> qw = QueryGenerator.initQueryWrapper(mesXslWeightRecord, req.getParameterMap());
qw.orderByDesc("create_time");
IPage<MesXslWeightRecord> 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<MesXslWeightRecord> 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<IPage<MesXslMixerMaterialTareStrategy>> tareStrategyAnonList(
MesXslMixerMaterialTareStrategy model,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslMixerMaterialTareStrategy> qw = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
qw.orderByDesc("effective_start_date", "create_time");
IPage<MesXslMixerMaterialTareStrategy> page = tareStrategyService.page(new Page<>(pageNo, pageSize), qw);
return Result.OK(page);
}
@Operation(summary = "密炼物料皮重策略-免密通过id查询")
@GetMapping("/xslmes/mesXslMixerMaterialTareStrategy/anon/queryById")
public Result<MesXslMixerMaterialTareStrategy> 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<String> 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<String> 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<String> 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<String> 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<IPage<MesXslUnit>> unitAnonList(
MesXslUnit mesXslUnit,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "1000") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslUnit> qw = QueryGenerator.initQueryWrapper(mesXslUnit, req.getParameterMap());
IPage<MesXslUnit> page = unitService.page(new Page<>(pageNo, pageSize), qw);
return Result.OK(page);
}
//update-end---author:cursor ---date:20250602 for【密炼物料皮重策略】桌面端单位下拉只读-----------
// ─────────────────────────── 车辆私有辅助 ────────────────────────────
private void applyWeightNetAndBillType(MesXslWeightRecord record) {

View File

@@ -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<MesXslMixerMaterialTareStrategy, IMesXslMixerMaterialTareStrategyService> {
@Autowired
private IMesXslMixerMaterialTareStrategyService mesXslMixerMaterialTareStrategyService;
@Autowired
private MesXslStompNotifyService stompNotify;
@Operation(summary = "MES密炼物料皮重策略-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<MesXslMixerMaterialTareStrategy>> queryPageList(
MesXslMixerMaterialTareStrategy model,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslMixerMaterialTareStrategy> queryWrapper =
QueryGenerator.initQueryWrapper(model, req.getParameterMap());
queryWrapper.orderByDesc("effective_start_date", "create_time");
Page<MesXslMixerMaterialTareStrategy> page = new Page<>(pageNo, pageSize);
IPage<MesXslMixerMaterialTareStrategy> 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<String> 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<String> 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<String> 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<String> 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<MesXslMixerMaterialTareStrategy> 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【密炼物料皮重策略】维护人自动回填当前登录用户-----------
}

View File

@@ -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<MesXslWeightRe
queryWrapper.orderByDesc("create_time");
Page<MesXslWeightRecord> page = new Page<>(pageNo, pageSize);
IPage<MesXslWeightRecord> 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<MesXslWeightRe
if (record == null) {
return Result.error("未找到对应数据");
}
fillEnteredWeight(Collections.singletonList(record));
fillWeightRecordDerivedFields(Collections.singletonList(record));
return Result.OK(record);
}
@@ -187,27 +185,7 @@ public class MesXslWeightRecordController extends JeecgController<MesXslWeightRe
}
}
/**
* 给一批磅单记录批量填充「已入场重量」transient 字段,不入库)。
* 数据来源所有引用本榜单bill_no 匹配)的原料入场记录的拆码明细的 (份数×每份重量) 累计。
* 实现上为避免 N+1先收集所有 billNo再一次 IN 查询累计。
*/
private void fillEnteredWeight(List<MesXslWeightRecord> records) {
if (records == null || records.isEmpty()) {
return;
}
List<String> billNos = records.stream()
.map(MesXslWeightRecord::getBillNo)
.filter(s -> s != null && !s.isBlank())
.distinct()
.collect(Collectors.toList());
if (billNos.isEmpty()) {
return;
}
Map<String, BigDecimal> 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<MesXslWeightRecord> records) {
rawMaterialEntryService.fillWeightRecordDerivedFields(records);
}
}

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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<MesXslMixerMaterialTareStrategy> {}

View File

@@ -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<MesXslMixerMaterialTareStrategy> {
//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【密炼物料皮重策略】保存前校验生效日期重叠含物料规格-----------
}

View File

@@ -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<MesXslRawMateri
*/
Map<String, BigDecimal> sumEnteredWeightByBillNos(Collection<String> billNos);
/**
* 按榜单号批量统计「货物皮重」(托盘及皮重合计累加)。
*
* @param billNos 榜单号集合
* @return billNo -&gt; 累计货物皮重;查不到的 billNo 不会出现在 map 中
*/
Map<String, BigDecimal> sumCargoTareByBillNos(Collection<String> billNos);
/**
* 给磅单列表/详情填充由原料入场记录衍生的 transient 字段(已入场重量、货物皮重)。
*/
void fillWeightRecordDerivedFields(List<MesXslWeightRecord> records);
/**
* 结存入库并汇总原材料库存。
* <p>

View File

@@ -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) {

View File

@@ -773,7 +773,7 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
//update-begin---author:cursor ---date:20260601 for【XSLMES-20260601-A62】生成混炼示方改为同步B/F段胶至胶料信息-----------
/**
* 生成混炼示方时,将 B 段/F 段胶料同步写入「胶料信息」(mes_material)
* 胶料类别取配合示方所选胶料代号」对应胶料的类别;不再写入密炼物料,也不再维护比重
* 除 ERP 编号留空、编码/名称为示方编号外,其余字段按配合示方所选胶料信息赋值
*/
private void syncGeneratedRubberMixerMaterial(
MesXslFormulaMixingGenerateRowVO row,
@@ -785,60 +785,128 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
}
String specCode = row.getSpecCode().trim();
boolean isFinalStage = "Q".equalsIgnoreCase(resolveMixingMaterialStep(row));
String categoryId = resolveGeneratedRubberCategoryId(formula);
String rubberName = resolveRubberName(formula);
MesMaterial sourceRubber = resolveSourceRubberMaterial(formula);
MesMaterial existing = findRubberMaterialByCodeOrName(specCode);
Date now = new Date();
if (existing != null) {
if (StringUtils.isNotBlank(categoryId)) {
existing.setCategoryId(categoryId);
//update-begin---author:cursor ---date:20260601 for【XSLMES-20260601-A62】更新已生成胶料时按原胶料复制字段-----------
if (sourceRubber != null) {
fillGeneratedRubberFromSource(existing, sourceRubber, specCode, now, true);
} else {
applyGeneratedRubberFallback(existing, formula, specCode, now);
}
if (StringUtils.isBlank(existing.getMaterialCode())) {
existing.setMaterialCode(specCode);
}
if (StringUtils.isBlank(existing.getMaterialName())) {
existing.setMaterialName(specCode);
}
if (StringUtils.isBlank(existing.getAliasName()) && StringUtils.isNotBlank(rubberName)) {
existing.setAliasName(rubberName);
}
existing.setEnableFlag(existing.getEnableFlag() == null ? 1 : existing.getEnableFlag());
existing.setUpdateTime(now);
//update-end---author:cursor ---date:20260601 for【XSLMES-20260601-A62】更新已生成胶料时按原胶料复制字段-----------
mesMaterialService.updateById(existing);
log.info(
"[混炼示方生成] 更新胶料信息 id={}, code={}, categoryId={}, isFinal={}",
"[混炼示方生成] 更新胶料信息 id={}, code={}, sourceId={}, isFinal={}",
existing.getId(),
specCode,
categoryId,
sourceRubber != null ? sourceRubber.getId() : null,
isFinalStage);
return;
}
MesMaterial material = new MesMaterial();
material.setMaterialCode(specCode);
material.setMaterialName(specCode);
material.setAliasName(StringUtils.isNotBlank(rubberName) ? rubberName : null);
material.setCategoryId(categoryId);
material.setEnableFlag(1);
material.setIsSpecialRubber(0);
material.setDelFlag(CommonConstant.DEL_FLAG_0);
material.setCreateTime(now);
material.setUpdateTime(now);
//update-begin---author:cursor ---date:20260601 for【XSLMES-20260601-A62】新增B/F段胶料按原胶料复制字段-----------
if (sourceRubber != null) {
fillGeneratedRubberFromSource(material, sourceRubber, specCode, now, false);
} else {
applyGeneratedRubberFallback(material, formula, specCode, now);
}
//update-end---author:cursor ---date:20260601 for【XSLMES-20260601-A62】新增B/F段胶料按原胶料复制字段-----------
mesMaterialService.save(material);
log.info(
"[混炼示方生成] 新增胶料信息 id={}, code={}, categoryId={}, isFinal={}",
"[混炼示方生成] 新增胶料信息 id={}, code={}, sourceId={}, isFinal={}",
material.getId(),
specCode,
categoryId,
sourceRubber != null ? sourceRubber.getId() : null,
isFinalStage);
}
/** 生成的 B/F 段胶料类别:取配合示方所选「胶料代号」对应胶料(mes_material)的类别 */
private String resolveGeneratedRubberCategoryId(MesXslFormulaSpec formula) {
/** 配合示方所选「胶料代号」对应的原胶料信息 */
private MesMaterial resolveSourceRubberMaterial(MesXslFormulaSpec formula) {
if (formula == null || StringUtils.isBlank(formula.getRubberMaterialId())) {
return null;
}
MesMaterial rubber = mesMaterialService.getById(formula.getRubberMaterialId());
if (rubber == null) {
return null;
}
if (rubber.getDelFlag() != null && rubber.getDelFlag().equals(CommonConstant.DEL_FLAG_1)) {
return null;
}
return rubber;
}
/**
* 将原胶料字段复制到生成的 B/F 段胶料:编码/名称=示方编号ERP 编号留空,不复制主键与审计字段。
*/
private void fillGeneratedRubberFromSource(
MesMaterial target, MesMaterial source, String specCode, Date now, boolean isUpdate) {
if (target == null || source == null || StringUtils.isBlank(specCode)) {
return;
}
String code = specCode.trim();
target.setMaterialCode(code);
target.setMaterialName(code);
target.setAliasName(source.getAliasName());
target.setShortName(source.getShortName());
target.setCategoryId(source.getCategoryId());
target.setMaterialGrade(source.getMaterialGrade());
target.setPlanPrice(source.getPlanPrice());
target.setMinStock(source.getMinStock());
target.setMaxStock(source.getMaxStock());
target.setBaseUnitId(source.getBaseUnitId());
target.setStatUnitId(source.getStatUnitId());
target.setUnitConvertRate(source.getUnitConvertRate());
target.setErpCode(null);
target.setFinalShelfLifeDays(source.getFinalShelfLifeDays());
target.setMasterShelfLifeDays(source.getMasterShelfLifeDays());
target.setMinStandingHours(source.getMinStandingHours());
target.setStandardCode(source.getStandardCode());
target.setOriginPlace(source.getOriginPlace());
target.setSupplierId(source.getSupplierId());
target.setCustomerId(source.getCustomerId());
target.setEnableFlag(source.getEnableFlag() != null ? source.getEnableFlag() : 1);
target.setIsSpecialRubber(source.getIsSpecialRubber() != null ? source.getIsSpecialRubber() : 0);
target.setSyncFromErpFlag(0);
target.setLastErpSyncTime(null);
target.setRemark(source.getRemark());
target.setTenantId(source.getTenantId());
target.setSysOrgCode(source.getSysOrgCode());
target.setUpdateTime(now);
if (!isUpdate) {
target.setDelFlag(CommonConstant.DEL_FLAG_0);
target.setCreateTime(now);
}
}
/** 未选择原胶料时的最小兜底(仅类别/状态) */
private void applyGeneratedRubberFallback(MesMaterial target, MesXslFormulaSpec formula, String specCode, Date now) {
if (target == null || StringUtils.isBlank(specCode)) {
return;
}
String code = specCode.trim();
target.setMaterialCode(code);
target.setMaterialName(code);
target.setCategoryId(resolveGeneratedRubberCategoryId(formula));
String rubberName = resolveRubberName(formula);
if (StringUtils.isNotBlank(rubberName)) {
target.setAliasName(rubberName);
}
target.setErpCode(null);
target.setEnableFlag(target.getEnableFlag() != null ? target.getEnableFlag() : 1);
target.setIsSpecialRubber(target.getIsSpecialRubber() != null ? target.getIsSpecialRubber() : 0);
target.setDelFlag(target.getDelFlag() != null ? target.getDelFlag() : CommonConstant.DEL_FLAG_0);
target.setUpdateTime(now);
if (target.getCreateTime() == null) {
target.setCreateTime(now);
}
}
/** 生成的 B/F 段胶料类别:取配合示方所选「胶料代号」对应胶料(mes_material)的类别 */
private String resolveGeneratedRubberCategoryId(MesXslFormulaSpec formula) {
MesMaterial rubber = resolveSourceRubberMaterial(formula);
return rubber != null ? rubber.getCategoryId() : null;
}

View File

@@ -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<MesXslMixerMaterialTareStrategyMapper, MesXslMixerMaterialTareStrategy>
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<MesXslMixerMaterialTareStrategy> 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【密炼物料皮重策略】保存前校验与冗余回填-----------
}

View File

@@ -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<String, BigDecimal> sumCargoTareByBillNos(Collection<String> billNos) {
if (billNos == null || billNos.isEmpty()) {
return Collections.emptyMap();
}
Set<String> distinct = billNos.stream()
.filter(s -> s != null && !s.isBlank())
.collect(Collectors.toCollection(HashSet::new));
if (distinct.isEmpty()) {
return Collections.emptyMap();
}
LambdaQueryWrapper<MesXslRawMaterialEntry> qw = new LambdaQueryWrapper<>();
qw.in(MesXslRawMaterialEntry::getBillNo, distinct)
.select(MesXslRawMaterialEntry::getBillNo, MesXslRawMaterialEntry::getPalletTareTotal);
List<MesXslRawMaterialEntry> rows = this.list(qw);
Map<String, BigDecimal> 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<MesXslWeightRecord> records) {
if (records == null || records.isEmpty()) {
return;
}
List<String> 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<String, BigDecimal> enteredMap = sumEnteredWeightByBillNos(billNos);
Map<String, BigDecimal> 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<String> ids) {

View File

@@ -541,6 +541,37 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTes
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
-- author:jiangxh---date:20250602--for: 【MES】停机记录建表+菜单+CRUD+列表录入维修结果弹窗 ---
jeecg-boot/db/mes-xsl-downtime-record-menu-permission.sql
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_117__mes_xsl_downtime_record.sql

View File

@@ -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
);

View File

@@ -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 '包装物重量';

View File

@@ -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`;

View File

@@ -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`;

View File

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

View File

@@ -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' },
};

View File

@@ -0,0 +1,153 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button
type="primary"
v-auth="'xslmes:mes_xsl_mixer_material_tare_strategy:add'"
@click="handleAdd"
preIcon="ant-design:plus-outlined"
>
新增
</a-button>
<a-button
type="primary"
v-auth="'xslmes:mes_xsl_mixer_material_tare_strategy:exportXls'"
preIcon="ant-design:export-outlined"
@click="onExportXls"
>
导出
</a-button>
<j-upload-button
type="primary"
v-auth="'xslmes:mes_xsl_mixer_material_tare_strategy:importExcel'"
preIcon="ant-design:import-outlined"
@click="onImportXls"
>
导入
</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined" />
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'xslmes:mes_xsl_mixer_material_tare_strategy:deleteBatch'">
批量操作
<Icon icon="mdi:chevron-down" />
</a-button>
</a-dropdown>
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'xslmes:mes_xsl_mixer_material_tare_strategy:edit',
},
]"
:dropDownActions="getDropDownAction(record)"
/>
</template>
</BasicTable>
<MesXslMixerMaterialTareStrategyModal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" name="xslmes-mesXslMixerMaterialTareStrategy" setup>
import { reactive } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import Icon from '/@/components/Icon';
import MesXslMixerMaterialTareStrategyModal from './components/MesXslMixerMaterialTareStrategyModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './MesXslMixerMaterialTareStrategy.data';
import { list, deleteOne, batchDelete, getExportUrl, getImportUrl } from './MesXslMixerMaterialTareStrategy.api';
const queryParam = reactive<any>({});
const [registerModal, { openModal }] = useModal();
const { tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '密炼物料皮重策略',
api: list,
columns,
canResize: true,
formConfig: {
schemas: searchFormSchema,
labelWidth: 110,
autoSubmitOnEnter: true,
showAdvancedButton: true,
fieldMapToTime: [['effectiveDateRange', ['effectiveEndDate_begin', 'effectiveStartDate_end'], 'YYYY-MM-DD']],
},
actionColumn: {
title: '操作',
dataIndex: 'action',
width: 160,
fixed: 'right',
slots: { customRender: 'action' },
},
beforeFetch: (params) => Object.assign(params, queryParam),
},
exportConfig: {
name: '密炼物料皮重策略',
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
const superQueryConfig = reactive(superQuerySchema);
function handleSuperQuery(params) {
Object.keys(params).forEach((k) => {
queryParam[k] = params[k];
});
reload();
}
function handleAdd() {
openModal(true, { isUpdate: false, showFooter: true });
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true, showFooter: true });
}
function handleDetail(record: Recordable) {
openModal(true, { record, isUpdate: true, showFooter: false });
}
function handleDelete(record: Recordable) {
deleteOne({ id: record.id }, handleSuccess);
}
function batchHandleDelete() {
batchDelete({ ids: selectedRowKeys.value.join(',') }, handleSuccess);
}
function handleSuccess() {
reload();
selectedRowKeys.value = [];
}
function getDropDownAction(record: Recordable) {
return [
{ label: '详情', onClick: handleDetail.bind(null, record) },
{
label: '删除',
popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) },
auth: 'xslmes:mes_xsl_mixer_material_tare_strategy:delete',
},
];
}
</script>

View File

@@ -0,0 +1,199 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="860" @ok="handleSubmit">
<BasicForm @register="registerForm">
<template #mixerMaterialPicker="{ model }">
<a-input-group compact style="display: flex; width: 100%">
<a-input
v-model:value="model.mixerMaterialName"
read-only
placeholder="请点击选择密炼物料"
style="flex: 1"
:disabled="isDetail"
/>
<a-button type="primary" :disabled="isDetail" @click="openMixerSelect">选择</a-button>
<a-button v-if="model.mixerMaterialId && !isDetail" @click="clearMixer(model)">清除</a-button>
</a-input-group>
</template>
<template #supplierPicker="{ model }">
<a-input-group compact style="display: flex; width: 100%">
<a-input
v-model:value="model.supplierName"
read-only
placeholder="请点击选择供应商"
style="flex: 1"
:disabled="isDetail"
/>
<a-button type="primary" :disabled="isDetail" @click="openSupplierSelect">选择</a-button>
<a-button v-if="model.supplierId && !isDetail" @click="clearSupplier(model)">清除</a-button>
</a-input-group>
</template>
<template #unitPicker="{ model }">
<a-input-group compact style="display: flex; width: 100%">
<a-input
v-model:value="model.unitName"
read-only
placeholder="请点击选择单位"
style="flex: 1"
:disabled="isDetail"
/>
<a-button type="primary" :disabled="isDetail" @click="openUnitSelect">选择</a-button>
<a-button v-if="model.unitId && !isDetail" @click="clearUnit(model)">清除</a-button>
</a-input-group>
</template>
</BasicForm>
<MesMixerMaterialSelectModal @register="registerMixerModal" @select="onMixerSelect" />
<MesXslSupplierSelectModal @register="registerSupplierModal" @select="onSupplierSelect" />
<MesXslUnitSelectModal @register="registerUnitModal" @select="onUnitSelect" />
</BasicModal>
</template>
<script lang="ts" setup>
import { computed, ref, unref } from 'vue';
import { BasicModal, useModalInner, useModal } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import MesMixerMaterialSelectModal from '/@/views/mes/material/modules/MesMixerMaterialSelectModal.vue';
import MesXslSupplierSelectModal from '/@/views/xslmes/mesXslVehicle/components/MesXslSupplierSelectModal.vue';
import MesXslUnitSelectModal from '/@/views/xslmes/mesXslVehicle/components/MesXslUnitSelectModal.vue';
import { formSchema } from '../MesXslMixerMaterialTareStrategy.data';
import { saveOrUpdate } from '../MesXslMixerMaterialTareStrategy.api';
const { createMessage } = useMessage();
const userStore = useUserStore();
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDetail = ref(false);
const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 130,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
const [registerMixerModal, { openModal: openMixerModal }] = useModal();
const [registerSupplierModal, { openModal: openSupplierModal }] = useModal();
const [registerUnitModal, { openModal: openUnitModal }] = useModal();
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
isUpdate.value = !!data?.isUpdate;
isDetail.value = !data?.showFooter;
if (unref(isUpdate)) {
await setFieldsValue({ ...data.record });
} else {
await setFieldsValue({ maintainBy: userStore.getUserInfo?.username || '' });
}
setProps({ disabled: !data?.showFooter });
});
const title = computed(() => (!unref(isUpdate) ? '新增' : unref(isDetail) ? '详情' : '编辑'));
function openMixerSelect() {
openMixerModal(true, {});
}
function onMixerSelect(payload: Recordable | null) {
if (!payload) {
return;
}
setFieldsValue({
mixerMaterialId: payload.mixerMaterialId,
mixerMaterialName: payload.materialName || '',
});
}
function clearMixer(model: Recordable) {
model.mixerMaterialId = '';
model.mixerMaterialName = '';
}
function openSupplierSelect() {
openSupplierModal(true, {});
}
function onSupplierSelect(payload: Recordable | null) {
if (!payload) {
return;
}
setFieldsValue({
supplierId: payload.supplierId || payload.id,
supplierName: payload.supplierName || '',
});
}
function clearSupplier(model: Recordable) {
model.supplierId = '';
model.supplierName = '';
}
function openUnitSelect() {
openUnitModal(true, {});
}
function onUnitSelect(payload: { unitId: string; unitName: string }) {
setFieldsValue({
unitId: payload.unitId || undefined,
unitName: payload.unitName || '',
});
}
function clearUnit(model: Recordable) {
model.unitId = '';
model.unitName = '';
}
async function handleSubmit() {
try {
const values = await validate();
if (!values.mixerMaterialId) {
createMessage.warning('请选择密炼物料');
return;
}
if (!values.supplierId) {
createMessage.warning('请选择供应商');
return;
}
if (!values.unitId) {
createMessage.warning('请选择单位');
return;
}
if (values.effectiveStartDate && values.effectiveEndDate && values.effectiveStartDate > values.effectiveEndDate) {
createMessage.warning('生效开始日期不能晚于截止日期');
return;
}
if (values.palletWeight != null && values.palletWeight < 0) {
createMessage.warning('托盘重量不能为负数');
return;
}
if (values.materialSpec) {
values.materialSpec = String(values.materialSpec).trim();
} else {
values.materialSpec = undefined;
}
setModalProps({ confirmLoading: true });
await saveOrUpdate(values, unref(isUpdate));
createMessage.success(unref(isUpdate) ? '编辑成功' : '新增成功');
closeModal();
emit('success');
} catch (e: any) {
if (e?.errorFields) {
const firstField = e.errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(e);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
:deep(.ant-input-number) {
width: 100%;
}
</style>

View File

@@ -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' },
};

View File

@@ -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' },
};

View File

@@ -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' },
};

View File

@@ -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 },
];

View File

@@ -21,6 +21,7 @@
"typeRoots": ["./node_modules/@types/", "./types","./node_modules"],
"noImplicitAny": false,
"skipLibCheck": true,
"noEmit": true,
"paths": {
"/@/*": ["src/*"],
"/#/*": ["types/*"],

3
scan_tare_strategy.json Normal file
View File

@@ -0,0 +1,3 @@
{
"error": "未找到匹配 'MesXslMixerMaterialTareStrategy' 的实体类,建议使用完整类名如 MesXslVehicle"
}

View File

@@ -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<MixerMaterialTareStrategyChangedPayload> { }

View File

@@ -0,0 +1,28 @@
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Services;
public interface IMixerMaterialTareStrategyService
{
Task<MixerMaterialTareStrategyPageResult> PageAsync(
int pageNo,
int pageSize,
string? mixerMaterialName = null,
string? supplierName = null,
CancellationToken ct = default);
Task<MesXslMixerMaterialTareStrategy?> GetByIdAsync(string id, CancellationToken ct = default);
Task<bool> AddAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default);
Task<bool> EditAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default);
Task<bool> DeleteAsync(string id, CancellationToken ct = default);
Task<List<MesXslUnit>> GetUnitsAsync(CancellationToken ct = default);
/// <summary>拉取全部策略(用于原料入场拆码明细自动/手动匹配)。</summary>
Task<IReadOnlyList<MesXslMixerMaterialTareStrategy>> GetAllForMatchAsync(CancellationToken ct = default);
}
public record MixerMaterialTareStrategyPageResult(
List<MesXslMixerMaterialTareStrategy> Records,
long Total,
int PageNo,
int PageSize);

View File

@@ -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;
}

View File

@@ -17,6 +17,10 @@ public class MesXslRawMaterialCard
public string? ManufacturerMaterialName { get; set; }
public string? ShelfLife { get; set; }
public decimal? TotalWeight { get; set; }
/// <summary>包装物皮重(KG)</summary>
public decimal? PackagingTare { get; set; }
/// <summary>托盘重量(KG)</summary>
public decimal? PalletWeight { get; set; }
public decimal? RemainingWeight { get; set; }
public int? RemainingQuantity { get; set; }

View File

@@ -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基础资料整票级单值独立专供明细行回填。

View File

@@ -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; }
}

View File

@@ -40,6 +40,17 @@ public class MesXslWeightRecord
/// </summary>
public double? EnteredWeight { get; set; }
/// <summary>
/// 货物皮重(KG) —— 后端/本地实时计算,不入库。
/// 来源所有引用本榜单BillNo的原料入场记录 pallet_tare_total托盘及皮重合计累加。
/// </summary>
public double? CargoTareWeight { get; set; }
/// <summary>
/// 原料重量(KG) —— 实时计算,不入库。公式:净重 - 货物皮重。
/// </summary>
public double? RawMaterialWeight { get; set; }
/// <summary>司机姓名</summary>
public string? DriverName { get; set; }

View File

@@ -46,6 +46,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
new SysMenu{ Id=1300150010901, Pid=1300150000101, Title="原材料卡片", Path="/xslmes/mesXslRawMaterialCard", Name="mesXslRawMaterialCard", Component="RawMaterialCardListView", Icon="&#xe7ce;", 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="&#xe7ce;", 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="&#xe7ce;", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
#endregion

View File

@@ -31,6 +31,7 @@ public class SysTenantMenuSeedData : ISqlSugarEntitySeedData<SysTenantMenu>
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},

View File

@@ -268,6 +268,8 @@ namespace YY.Admin.Core.SqlSugar
if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config);
// 关闭全量种子时首启可能无菜单数据;补一份基准菜单,避免打包版本左侧空白
EnsureBaselineSysMenuSeed(db, config);
// 旧库升级:按种子补全缺失菜单及租户/角色授权(仅插入缺失项)
EnsureIncrementalDesktopMenuSeed(db, config);
}
/// <summary>
@@ -392,6 +394,106 @@ namespace YY.Admin.Core.SqlSugar
}
}
/// <summary>
/// 旧库升级sys_menu 已有数据时,按 SysMenuSeedData 补全缺失菜单,并同步租户菜单、管理员角色菜单授权。
/// </summary>
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<SysMenu>().ClearFilter().Count() == 0)
{
return;
}
var seedMenus = new SysMenuSeedData().HasData().ToList();
var existingMenuIds = dbProvider.Queryable<SysMenu>().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<SysTenantMenu>()
.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<SysRole>().OrderBy(r => r.Id).First();
if (adminRole == null)
{
return;
}
var existingRoleMenuIds = dbProvider.Queryable<SysRoleMenu>()
.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
{
// 启动阶段不因增量菜单失败而阻断
}
}
/// <summary>
/// 兼容旧库:补齐桌面端「登录设置」所需的 sys_config 配置项(升级前库可能缺少这些 code
/// </summary>

View File

@@ -0,0 +1,49 @@
using YY.Admin.Core.Entity;
namespace YY.Admin.Core.Util;
/// <summary>
/// 「货物皮重」桌面端本地累计计算器。
/// 与后端 IMesXslRawMaterialEntryService.sumCargoTareByBillNos 保持同一口径:
/// 同一榜单BillNo下所有原料入场记录的 PalletTareTotal托盘及皮重合计累加。
/// </summary>
public static class CargoTareWeightCalculator
{
/// <summary>按 BillNo 分组累计「货物皮重」。</summary>
public static Dictionary<string, double> SumByBillNos(
IEnumerable<MesXslRawMaterialEntry> entries,
IEnumerable<string?> billNos)
{
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var b in billNos)
{
if (!string.IsNullOrWhiteSpace(b)) keys.Add(b!);
}
var result = new Dictionary<string, double>(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;
}
}

View File

@@ -0,0 +1,80 @@
using System.Globalization;
using YY.Admin.Core.Entity;
namespace YY.Admin.Services.Service.MixerMaterialTareStrategy;
/// <summary>
/// 密炼物料皮重策略匹配:按密炼物料、供应商、入场日期、每份重量(对应策略物料规格)筛选。
/// </summary>
public static class MixerMaterialTareStrategyMatcher
{
public static List<MesXslMixerMaterialTareStrategy> FilterCandidates(
IEnumerable<MesXslMixerMaterialTareStrategy> strategies,
string? mixerMaterialId,
string? supplierId,
DateTime? entryDate,
double? portionWeight)
{
if (string.IsNullOrWhiteSpace(mixerMaterialId) || string.IsNullOrWhiteSpace(supplierId))
{
return new List<MesXslMixerMaterialTareStrategy>();
}
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<MesXslMixerMaterialTareStrategy> 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;
}
/// <summary>策略「物料规格」与拆码明细「每份重量」比对(支持数值或文本形式)。</summary>
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);
}
}

View File

@@ -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<TareStrategyPendingOperation> _pendingOps = new();
private List<MesXslMixerMaterialTareStrategy> _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<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
public async Task<MixerMaterialTareStrategyPageResult> PageAsync(
int pageNo, int pageSize,
string? mixerMaterialName = null, string? supplierName = null,
CancellationToken ct = default)
{
List<MesXslMixerMaterialTareStrategy>? 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<IReadOnlyList<MesXslMixerMaterialTareStrategy>> GetAllForMatchAsync(CancellationToken ct = default)
{
var page = await PageAsync(1, 10000, null, null, ct).ConfigureAwait(false);
return page.Records;
}
public async Task<MesXslMixerMaterialTareStrategy?> 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<MesXslMixerMaterialTareStrategy>(_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<bool> 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<bool> 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<bool> 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<List<MesXslUnit>> GetUnitsAsync(CancellationToken ct = default)
{
if (!_networkMonitor.IsOnline)
return new List<MesXslUnit>();
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<List<MesXslUnit>>(_jsonOpts) ?? new();
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略] 单位列表拉取失败:{ex.Message}");
return new List<MesXslUnit>();
}
}
private async Task<List<MesXslMixerMaterialTareStrategy>> 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<List<MesXslMixerMaterialTareStrategy>>(_jsonOpts) ?? new();
}
private async Task<bool> 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<bool> 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<bool> 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<bool> 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<MixerMaterialTareStrategyChangedEvent>()
.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<SyncConflictEvent>()
.Publish(new SyncConflictPayload
{
EntityName = "密炼物料皮重策略",
PushedCount = pushResult.PushedCount,
ConflictCount = pushResult.ConflictCount,
NewRecordsPushed = pushResult.NewRecordsPushed
});
}
}
private async Task<PushPendingResult> PushPendingOnReconnectAsync(CancellationToken ct)
{
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false))
return new PushPendingResult(0, 0, 0);
try
{
List<TareStrategyPendingOperation> 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<PendingReplayResult> 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<PendingReplayResult> 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<PendingReplayResult> 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<PendingReplayResult> 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<MesXslMixerMaterialTareStrategy?> 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<MesXslMixerMaterialTareStrategy>(_jsonOpts);
return null;
}
catch { return null; }
}
private static List<MesXslMixerMaterialTareStrategy> ApplyFilters(
List<MesXslMixerMaterialTareStrategy> source,
string? mixerMaterialName, string? supplierName)
{
IEnumerable<MesXslMixerMaterialTareStrategy> 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<MesXslMixerMaterialTareStrategy> ApplyPendingOpsSnapshotUnsafe(List<MesXslMixerMaterialTareStrategy> 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<List<TareStrategyPendingOperation>>(
File.ReadAllText(_pendingOpsFilePath), _jsonOpts) ?? new();
}
catch { _pendingOps = new(); }
}
private void LoadCacheFromDisk()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
_localCache = JsonSerializer.Deserialize<List<MesXslMixerMaterialTareStrategy>>(
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<DateTime?>
{
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();
}
}
}

View File

@@ -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<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
_eventAggregator.GetEvent<NetworkStatusChangedEvent>()
.Subscribe(OnNetworkStatusChanged, ThreadOption.BackgroundThread);
pollManager.Register("密炼物料皮重策略", () =>
{
_eventAggregator.GetEvent<MixerMaterialTareStrategyChangedEvent>()
.Publish(new MixerMaterialTareStrategyChangedPayload { Action = "poll" });
return Task.CompletedTask;
});
_logger.Information("[密炼物料皮重策略推送] MixerMaterialTareStrategySyncCoordinator 已启动");
}
private void OnNetworkStatusChanged(NetworkStatusChangedPayload payload)
{
if (!payload.IsOnline) return;
_logger.Information("[密炼物料皮重策略推送] 网络恢复,触发补偿刷新");
_eventAggregator.GetEvent<MixerMaterialTareStrategyChangedEvent>()
.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<MixerMaterialTareStrategyChangedEvent>().Publish(changed);
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略推送] 处理STOMP命令失败{ex.Message}");
}
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
}
/// <summary>
/// 给一批磅单记录批量填充「已入场重量」
/// 数据来源:本地 RawMaterialEntry 缓存的拆码明细字段totalPortions / portionWeight
/// 与后端 sumEnteredWeightByBillNos 同口径,确保离线场景也能正确显示。
/// 给一批磅单记录批量填充由原料入场记录衍生的 transient 字段
/// </summary>
private void FillEnteredWeightFromLocalEntries(List<MesXslWeightRecord> records)
private void FillEntryDerivedFieldsFromLocalEntries(List<MesXslWeightRecord> 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);
}
}
/// <summary>原料重量 = 净重 - 货物皮重(不落库)</summary>
private static void ApplyRawMaterialWeight(MesXslWeightRecord record)
{
record.RawMaterialWeight = record.NetWeight.HasValue
? record.NetWeight.Value - (record.CargoTareWeight ?? 0d)
: null;
}
public async Task<MesXslWeightRecord?> GetByIdAsync(string id, CancellationToken ct = default)
{
MesXslWeightRecord? record = null;
@@ -190,7 +203,7 @@ public class WeightRecordService : IWeightRecordService, ISingletonDependency
if (record != null)
{
FillEnteredWeightFromLocalEntries(new List<MesXslWeightRecord> { record });
FillEntryDerivedFieldsFromLocalEntries(new List<MesXslWeightRecord> { 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,

View File

@@ -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"),

View File

@@ -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<RawMaterialCardListView>();
// 库区管理
containerRegistry.RegisterForNavigation<WarehouseAreaListView>();
// 密炼物料皮重策略
containerRegistry.RegisterForNavigation<MixerMaterialTareStrategyListView>();
// 打印设置
containerRegistry.RegisterForNavigation<PrintSettingsView>();
// 打印模板列表

View File

@@ -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<IWarehouseAreaService, WarehouseAreaService>();
containerRegistry.RegisterSingleton<WarehouseAreaSyncCoordinator>();
// 密炼物料皮重策略:免密 API 直连 + STOMP 实时通知
containerRegistry.RegisterSingleton<IMixerMaterialTareStrategyService, MixerMaterialTareStrategyService>();
containerRegistry.RegisterSingleton<MixerMaterialTareStrategySyncCoordinator>();
// 分类字典:启动同步 + 断线重连补刷
containerRegistry.RegisterSingleton<CategorySyncCoordinator>();
// 数据字典:启动同步 + 断线重连补刷
@@ -143,6 +147,8 @@ public class SyncModule : IModule
_ = containerProvider.Resolve<RawMaterialCardSyncCoordinator>();
// 强制实例化库区同步协调器
_ = containerProvider.Resolve<WarehouseAreaSyncCoordinator>();
// 强制实例化密炼物料皮重策略同步协调器
_ = containerProvider.Resolve<MixerMaterialTareStrategySyncCoordinator>();
// 强制实例化分类字典同步协调器
_ = containerProvider.Resolve<CategorySyncCoordinator>();
// 强制实例化数据字典同步协调器

View File

@@ -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",

View File

@@ -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<bool>
{
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<MesXslUnit> 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<RawMaterialEntryMaterialPickerDialogView>()
.Initialize<RawMaterialEntryMaterialPickerDialogViewModel>(vm =>
{
pickerVm = vm;
vm.Initialize(Strategy?.MixerMaterialId, Strategy?.MixerMaterialName);
})
.GetResultAsync<bool>();
}
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<SupplierPickerDialogView>()
.Initialize<SupplierPickerDialogViewModel>(vm => pickerVm = vm)
.GetResultAsync<bool>();
}
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}");
}
}
}

View File

@@ -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<MesXslMixerMaterialTareStrategy> _strategies = new();
public ObservableCollection<MesXslMixerMaterialTareStrategy> 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<MesXslMixerMaterialTareStrategy> EditCommand { get; }
public DelegateCommand<MesXslMixerMaterialTareStrategy> 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<MesXslMixerMaterialTareStrategy>(async s => await ShowEditDialogAsync(s));
DeleteCommand = new DelegateCommand<MesXslMixerMaterialTareStrategy>(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<MixerMaterialTareStrategyChangedEvent>()
.Subscribe(async p => await OnChangedAsync(p), ThreadOption.UIThread);
_syncConflictToken = _eventAggregator.GetEvent<SyncConflictEvent>()
.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<string>();
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<MesXslMixerMaterialTareStrategy>(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<MixerMaterialTareStrategyEditDialogView>()
.Initialize<MixerMaterialTareStrategyEditDialogViewModel>(vm => vm.InitializeForAdd())
.GetResultAsync<bool>();
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<MixerMaterialTareStrategyEditDialogView>()
.Initialize<MixerMaterialTareStrategyEditDialogViewModel>(vm => vm.InitializeForEdit(strategy))
.GetResultAsync<bool>();
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<MixerMaterialTareStrategyChangedEvent>().Unsubscribe(_changedToken);
_changedToken = null;
}
if (_syncConflictToken != null)
{
_eventAggregator.GetEvent<SyncConflictEvent>().Unsubscribe(_syncConflictToken);
_syncConflictToken = null;
}
}
}

View File

@@ -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,

View File

@@ -9,6 +9,7 @@ using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Services;
using YY.Admin.Services.Service;
using YY.Admin.Services.Service.MixerMaterialTareStrategy;
using YY.Admin.Views.RawMaterialEntry;
using YY.Admin.ViewModels.WeightRecord;
using YY.Admin.Views.WeightRecord;
@@ -22,8 +23,9 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
protected IRawMaterialEntryService EntryService => _entryService;
private readonly IJeecgDictSyncService _dictSyncService;
private readonly IMixerMaterialService _mixerMaterialService;
// 加载完物料后用于回填 Edit 模式选中项
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
private readonly ISupplierService _supplierService;
private bool _suppressTareStrategyRefresh;
protected string? _pendingMaterialId;
private MesXslRawMaterialEntry? _entry;
@@ -49,6 +51,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
RaisePropertyChanged(nameof(SelectedMaterialDisplay));
RaisePropertyChanged(nameof(HasSelectedMaterial));
_ = RefreshAllRowsTareStrategyAsync();
// 新增模式自动生成条码/批次号
if (IsAddMode && !string.IsNullOrEmpty(value.MaterialCode))
_ = AutoGenerateBarcodeAsync(value.MaterialCode);
@@ -106,6 +110,27 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
}
}
public DateTime? EntryTimeInput
{
get => Entry?.EntryTime;
set
{
if (Entry == null || Entry.EntryTime == value) return;
Entry.EntryTime = value;
RaisePropertyChanged(nameof(Entry));
_ = RefreshAllRowsTareStrategyAsync();
}
}
/// <summary>基础资料「托盘及皮重(合计)」= Σ 份数 × (包装物皮重 + 托盘重量)。</summary>
public double PalletTareTotalDisplay => SplitCodeDetails.Sum(row =>
{
var portions = row.Portions ?? 0;
var packaging = row.PackagingTare ?? 0d;
var pallet = row.PalletWeight ?? 0d;
return portions * (packaging + pallet);
});
public ObservableCollection<MesMixerMaterial> MaterialOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> TestResultOptions { get; } = new();
public ObservableCollection<KeyValuePair<string, string>> TestStatusOptions { get; } = new();
@@ -174,21 +199,27 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
public DelegateCommand ClearWeightRecordCommand { get; }
public DelegateCommand OpenSupplierPickerCommand { get; }
public DelegateCommand ClearSupplierCommand { get; }
/// <summary>
/// 拆码明细 - 库位选择命令弹出「库区选择」弹窗单选。CommandParameter 为当前行 RawMaterialSplitDetailItem。
/// </summary>
/// <summary>拆码明细 - 库位选择命令弹出「库区选择」弹窗单选。CommandParameter 为当前行 RawMaterialSplitDetailItem。</summary>
public DelegateCommand<RawMaterialSplitDetailItem> OpenWarehouseAreaPickerCommand { get; }
/// <summary>拆码明细 - 手动选择皮重策略。</summary>
public DelegateCommand<RawMaterialSplitDetailItem> OpenTareStrategyPickerCommand { get; }
/// <summary>拆码明细 - 清除手动策略并按规则重新自动匹配。</summary>
public DelegateCommand<RawMaterialSplitDetailItem> ResetTareStrategyCommand { get; }
public RawMaterialEntryEditDialogViewModel(
IRawMaterialEntryService entryService,
IJeecgDictSyncService dictSyncService,
IMixerMaterialService mixerMaterialService,
IMixerMaterialTareStrategyService tareStrategyService,
ISupplierService supplierService,
IContainerExtension container,
IRegionManager regionManager) : base(container, regionManager)
{
_entryService = entryService;
_dictSyncService = dictSyncService;
_mixerMaterialService = mixerMaterialService;
_tareStrategyService = tareStrategyService;
_supplierService = supplierService;
SaveCommand = new DelegateCommand(async () => await SaveAsync());
CancelCommand = new DelegateCommand(() => CloseAction?.Invoke());
ResetCommand = new DelegateCommand(InitializeForAdd);
@@ -201,6 +232,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
OpenSupplierPickerCommand = new DelegateCommand(async () => await OpenSupplierPickerAsync());
ClearSupplierCommand = new DelegateCommand(ClearSupplierSelection);
OpenWarehouseAreaPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenWarehouseAreaPickerAsync(row));
OpenTareStrategyPickerCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await OpenTareStrategyPickerAsync(row));
ResetTareStrategyCommand = new DelegateCommand<RawMaterialSplitDetailItem>(async row => await ResetTareStrategyAsync(row));
SplitCodeDetails.CollectionChanged += OnSplitCodeDetailsCollectionChanged;
_ = LoadAllAsync();
}
@@ -349,6 +382,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
RaisePropertyChanged(nameof(IsAddMode));
RaisePropertyChanged(nameof(DialogTitle));
RaisePropertyChanged(nameof(TotalWeightInput));
RaisePropertyChanged(nameof(EntryTimeInput));
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
@@ -367,8 +402,13 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
MaterialId = entry.MaterialId, MaterialCode = entry.MaterialCode, MaterialName = entry.MaterialName,
SupplyCustomer = entry.SupplyCustomer, SupplierId = entry.SupplierId, SupplierName = entry.SupplierName,
ManufacturerMaterialName = entry.ManufacturerMaterialName,
ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, TotalPortions = entry.TotalPortions,
PortionWeight = entry.PortionWeight, PortionPackages = entry.PortionPackages,
ShelfLife = entry.ShelfLife, TotalWeight = entry.TotalWeight, PalletTareTotal = entry.PalletTareTotal,
TotalPortions = entry.TotalPortions,
PortionWeight = entry.PortionWeight,
PortionPackagingTare = entry.PortionPackagingTare,
PortionPalletWeight = entry.PortionPalletWeight,
PortionTareStrategyIds = entry.PortionTareStrategyIds,
PortionPackages = entry.PortionPackages,
PortionWarehouseLocations = entry.PortionWarehouseLocations,
PortionDetailIds = entry.PortionDetailIds,
PortionCardFlags = entry.PortionCardFlags,
@@ -398,6 +438,8 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
RaisePropertyChanged(nameof(IsAddMode));
RaisePropertyChanged(nameof(DialogTitle));
RaisePropertyChanged(nameof(TotalWeightInput));
RaisePropertyChanged(nameof(EntryTimeInput));
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
RaisePropertyChanged(nameof(IsSpecialAdoptionValue));
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
@@ -503,6 +545,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.BillNo = selected.BillNo;
Entry.SupplierName = selected.SenderUnit;
Entry.SupplierId = null;
await ApplySupplierFromDisplayNameAsync(selected.SenderUnit);
// 选择榜单后,自动把「剩余可入场量 = 净重 - 已入场重量」带入「总重」,
// 用户仍可手动编辑;若净重为空则保留原值,避免误清空。
if (selected.NetWeight.HasValue)
@@ -514,6 +557,76 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
}
RaisePropertyChanged(nameof(Entry));
RaisePropertyChanged(nameof(TotalWeightInput));
RaisePropertyChanged(nameof(EntryTimeInput));
RaisePropertyChanged(nameof(PalletTareTotalDisplay));
await RefreshAllRowsTareStrategyAsync();
}
/// <summary>
/// 按供应商全称/简称反查 ID榜单「发货单位」与手动填写的供应商名称均可用
/// </summary>
private async Task ApplySupplierFromDisplayNameAsync(string? displayName)
{
if (Entry == null || string.IsNullOrWhiteSpace(displayName))
{
return;
}
var supplier = await FindSupplierByDisplayNameAsync(displayName);
if (supplier == null)
{
return;
}
Entry.SupplierId = supplier.Id;
Entry.SupplierName = supplier.SupplierName
?? supplier.SupplierShortName
?? displayName.Trim();
RaisePropertyChanged(nameof(Entry));
}
private async Task<MesXslSupplier?> FindSupplierByDisplayNameAsync(string? displayName)
{
if (string.IsNullOrWhiteSpace(displayName))
{
return null;
}
var name = displayName.Trim();
try
{
var page = await _supplierService.PageAsync(1, 5000);
var exact = page.Records.FirstOrDefault(s =>
string.Equals(s.SupplierName?.Trim(), name, StringComparison.OrdinalIgnoreCase)
|| string.Equals(s.SupplierShortName?.Trim(), name, StringComparison.OrdinalIgnoreCase));
if (exact != null)
{
return exact;
}
return page.Records.FirstOrDefault(s =>
(s.SupplierName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase)
|| (s.SupplierShortName ?? "").Contains(name, StringComparison.OrdinalIgnoreCase)
|| name.Contains(s.SupplierShortName ?? "", StringComparison.OrdinalIgnoreCase)
|| name.Contains(s.SupplierName ?? "", StringComparison.OrdinalIgnoreCase));
}
catch
{
return null;
}
}
/// <summary>
/// 皮重策略匹配前确保 SupplierId 已解析(榜单仅带出名称时补全 ID
/// </summary>
private async Task EnsureSupplierIdForTareMatchAsync()
{
if (Entry == null || !string.IsNullOrWhiteSpace(Entry.SupplierId))
{
return;
}
await ApplySupplierFromDisplayNameAsync(Entry.SupplierName);
}
/// <summary>
@@ -617,6 +730,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.SupplierId = null;
Entry.SupplierName = null;
RaisePropertyChanged(nameof(Entry));
_ = RefreshAllRowsTareStrategyAsync();
}
private async Task OpenSupplierPickerAsync()
@@ -638,6 +752,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.SupplierId = selected.Id;
Entry.SupplierName = selected.SupplierName;
RaisePropertyChanged(nameof(Entry));
await RefreshAllRowsTareStrategyAsync();
}
private void ClearSupplierSelection()
@@ -646,8 +761,128 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.SupplierId = null;
Entry.SupplierName = null;
RaisePropertyChanged(nameof(Entry));
_ = RefreshAllRowsTareStrategyAsync();
}
private async Task RefreshAllRowsTareStrategyAsync()
{
if (_suppressTareStrategyRefresh) return;
await EnsureSupplierIdForTareMatchAsync();
foreach (var row in SplitCodeDetails.ToList())
{
if (!row.IsManualTareStrategy)
await ApplyAutoTareStrategyToRowAsync(row);
}
}
private async Task ApplyAutoTareStrategyToRowAsync(RawMaterialSplitDetailItem row)
{
if (_suppressTareStrategyRefresh || row.IsManualTareStrategy || row.HasCard) return;
try
{
var strategies = await _tareStrategyService.GetAllForMatchAsync();
var match = MixerMaterialTareStrategyMatcher.PickBestMatch(
strategies,
Entry?.MaterialId,
Entry?.SupplierId,
Entry?.EntryTime,
row.PortionWeight);
ApplyTareStrategyToRow(row, match, manual: false);
}
catch
{
ApplyTareStrategyToRow(row, null, manual: false);
}
RaisePalletTareTotalChanged();
}
private void ApplyTareStrategyToRow(
RawMaterialSplitDetailItem row,
MesXslMixerMaterialTareStrategy? strategy,
bool manual)
{
if (strategy == null)
{
row.PackagingTare = 0d;
row.PalletWeight = 0d;
row.TareStrategyId = null;
row.TareStrategyDisplay = "未匹配(0)";
}
else
{
row.PackagingTare = strategy.TareWeight.HasValue ? (double)strategy.TareWeight.Value : 0d;
row.PalletWeight = strategy.PalletWeight.HasValue ? (double)strategy.PalletWeight.Value : 0d;
row.TareStrategyId = strategy.Id;
row.TareStrategyDisplay = BuildTareStrategyDisplay(strategy);
}
row.IsManualTareStrategy = manual && strategy != null;
}
private static string BuildTareStrategyDisplay(MesXslMixerMaterialTareStrategy strategy)
{
var spec = string.IsNullOrWhiteSpace(strategy.MaterialSpec) ? "-" : strategy.MaterialSpec;
var pkg = strategy.TareWeight?.ToString("0.##") ?? "0";
var pallet = strategy.PalletWeight?.ToString("0.##") ?? "0";
return $"规格:{spec} 包装:{pkg} 托盘:{pallet}";
}
private async Task OpenTareStrategyPickerAsync(RawMaterialSplitDetailItem? row)
{
if (row == null || row.HasCard) return;
if (string.IsNullOrWhiteSpace(Entry?.MaterialId))
{
HandyControl.Controls.MessageBox.Warning("请先选择密炼物料!");
return;
}
await EnsureSupplierIdForTareMatchAsync();
if (string.IsNullOrWhiteSpace(Entry?.SupplierId))
{
HandyControl.Controls.MessageBox.Warning("未能匹配到供应商档案,请手动选择供应商,或检查榜单发货单位是否与供应商简称/全称一致!");
return;
}
TareStrategyPickerDialogViewModel? pickerVm = null;
bool confirmed;
try
{
confirmed = await SuspendEmbeddedPrintPreviewAirspaceWhileAsync(async () =>
{
return await HandyControl.Controls.Dialog.Show<TareStrategyPickerDialogView>()
.Initialize<TareStrategyPickerDialogViewModel>(vm =>
{
pickerVm = vm;
vm.Initialize(Entry!.MaterialId, Entry.SupplierId, Entry.EntryTime, row.PortionWeight, row.TareStrategyId);
})
.GetResultAsync<bool>();
});
}
catch { return; }
if (!confirmed) return;
if (pickerVm?.SelectedStrategy == null)
{
row.IsManualTareStrategy = false;
await ApplyAutoTareStrategyToRowAsync(row);
return;
}
ApplyTareStrategyToRow(row, pickerVm.SelectedStrategy, manual: true);
RaisePalletTareTotalChanged();
}
private async Task ResetTareStrategyAsync(RawMaterialSplitDetailItem? row)
{
if (row == null || row.HasCard) return;
row.IsManualTareStrategy = false;
await ApplyAutoTareStrategyToRowAsync(row);
}
private void RaisePalletTareTotalChanged() => RaisePropertyChanged(nameof(PalletTareTotalDisplay));
private void RecalculateShelfLife(MesMixerMaterial? material)
{
if (Entry == null || material?.ShelfLifeDays == null || material.ShelfLifeDays <= 0)
@@ -719,63 +954,68 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
protected void InitializeSplitCodeDetailsFromEntry()
{
SplitCodeDetails.Clear();
// 六个字段都是按拆码明细多行拼接的字符串(末尾带 /),解析回明细列表
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations);
var idsArr = SplitJoinedValues(Entry?.PortionDetailIds);
var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags);
// 历史记录可能没存 PortionCardFlags用 print_flag 推断(与原行为兼容)
var fallbackToPrintFlag = cardFlagsArr.Length == 0
&& string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length),
Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length)));
for (var i = 0; i < rowCount; i++)
_suppressTareStrategyRefresh = true;
try
{
var locationFromArr = GetAt(locationsArr, i);
// 兼容历史数据:明细库位拼接字段为空时,首行回退到 Entry.WarehouseLocation
// (早期版本里整票级库位曾被反写过,避免老记录打开后明细全空)
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
? Entry?.WarehouseLocation
: locationFromArr;
SplitCodeDetails.Clear();
var item = new RawMaterialSplitDetailItem
var portionsArr = SplitJoinedValues(Entry?.TotalPortions);
var weightArr = SplitJoinedValues(Entry?.PortionWeight);
var packagingTareArr = SplitJoinedValues(Entry?.PortionPackagingTare);
var palletWeightArr = SplitJoinedValues(Entry?.PortionPalletWeight);
var strategyIdsArr = SplitJoinedValues(Entry?.PortionTareStrategyIds);
var packagesArr = SplitJoinedValues(Entry?.PortionPackages);
var locationsArr = SplitJoinedValues(Entry?.PortionWarehouseLocations);
var idsArr = SplitJoinedValues(Entry?.PortionDetailIds);
var cardFlagsArr = SplitJoinedValues(Entry?.PortionCardFlags);
var fallbackToPrintFlag = cardFlagsArr.Length == 0
&& string.Equals(Entry?.PrintFlag, "1", StringComparison.Ordinal);
var rowCount = Math.Max(1, Math.Max(Math.Max(portionsArr.Length, weightArr.Length),
Math.Max(Math.Max(packagesArr.Length, locationsArr.Length), idsArr.Length)));
for (var i = 0; i < rowCount; i++)
{
Portions = TryParseInt(GetAt(portionsArr, i)),
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
WarehouseLocation = locationFallback,
};
// 仅当后端已持久化了该行 ID 时才覆盖默认值,确保跨次保持稳定;
// 历史记录无 ID 字段时使用构造期生成的新 GUID之后保存会回填
var existingId = GetAt(idsArr, i);
if (!string.IsNullOrWhiteSpace(existingId))
{
item.Id = existingId;
var locationFromArr = GetAt(locationsArr, i);
var locationFallback = (i == 0 && string.IsNullOrWhiteSpace(locationFromArr))
? Entry?.WarehouseLocation
: locationFromArr;
var strategyId = GetAt(strategyIdsArr, i);
var item = new RawMaterialSplitDetailItem
{
Portions = TryParseInt(GetAt(portionsArr, i)),
PortionWeight = TryParseDouble(GetAt(weightArr, i)),
PortionPackages = TryParseInt(GetAt(packagesArr, i)),
WarehouseLocation = locationFallback,
PackagingTare = TryParseDouble(GetAt(packagingTareArr, i)) ?? 0d,
PalletWeight = TryParseDouble(GetAt(palletWeightArr, i)) ?? 0d,
TareStrategyId = strategyId,
IsManualTareStrategy = !string.IsNullOrWhiteSpace(strategyId),
};
item.TareStrategyDisplay = string.IsNullOrWhiteSpace(strategyId)
? "未匹配(0)"
: $"策略ID:{strategyId}";
var existingId = GetAt(idsArr, i);
if (!string.IsNullOrWhiteSpace(existingId))
item.Id = existingId;
var flagAt = GetAt(cardFlagsArr, i);
if (!string.IsNullOrWhiteSpace(flagAt))
item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal);
else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId))
item.HasCard = true;
SplitCodeDetails.Add(item);
}
// HasCard 行级解析(优先级 1从 PortionCardFlags 按位读取,"1"=已生成。
// 这是「保存后新增未生成行不被误判为已打印」的关键 — 新增行保存时 HasCard=false 持久化为 "0"
// 重新加载时回填 false「生成原材料卡片」就能正确把它列入待生成清单。
var flagAt = GetAt(cardFlagsArr, i);
if (!string.IsNullOrWhiteSpace(flagAt))
{
item.HasCard = string.Equals(flagAt, "1", StringComparison.Ordinal);
}
else if (fallbackToPrintFlag && !string.IsNullOrWhiteSpace(existingId))
{
// 优先级 2兼容历史记录未存 PortionCardFlags 时,沿用旧逻辑——
// 持久化 ID 存在 + 整票已打印 ⇒ 视为已生成卡片。
item.HasCard = true;
}
// 否则保持构造默认值 false新增态、未打印整票等场景
SplitCodeDetails.Add(item);
RaisePropertyChanged(nameof(SplitCodeTableHeight));
RaisePalletTareTotalChanged();
}
finally
{
_suppressTareStrategyRefresh = false;
}
RaisePropertyChanged(nameof(SplitCodeTableHeight));
}
private static string[] SplitJoinedValues(string? value)
@@ -798,8 +1038,10 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
private void AddSplitDetailRow()
{
SplitCodeDetails.Add(new RawMaterialSplitDetailItem());
var row = new RawMaterialSplitDetailItem();
SplitCodeDetails.Add(row);
RaisePropertyChanged(nameof(SplitCodeTableHeight));
_ = ApplyAutoTareStrategyToRowAsync(row);
}
private void RemoveSplitDetailRow(RawMaterialSplitDetailItem? item)
@@ -841,32 +1083,52 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
private void OnSplitDetailItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// 用户填写或修改某行的「份数」时,按公式自动算出该行「每份重量」:
// 每份重量 = (总重 - Σ其他行的份数×每份重量) / 当前行份数
// 用户修改某行的「份数」时:仅当该行「每份重量」为空才按公式自动计算
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.Portions)
&& sender is RawMaterialSplitDetailItem row)
{
RecalculatePortionWeightForRow(row);
}
if (e.PropertyName == nameof(RawMaterialSplitDetailItem.PortionWeight)
&& sender is RawMaterialSplitDetailItem weightRow)
{
// 清空每份重量后,若份数已填则立即按公式重算(配合 PortionWeightText 空值绑定)
if (!weightRow.PortionWeight.HasValue)
{
RecalculatePortionWeightForRow(weightRow);
}
if (!weightRow.IsManualTareStrategy && !weightRow.HasCard)
{
_ = ApplyAutoTareStrategyToRowAsync(weightRow);
}
}
if (e.PropertyName is nameof(RawMaterialSplitDetailItem.Portions)
or nameof(RawMaterialSplitDetailItem.PortionWeight)
or nameof(RawMaterialSplitDetailItem.PortionPackages))
or nameof(RawMaterialSplitDetailItem.PortionPackages)
or nameof(RawMaterialSplitDetailItem.PackagingTare)
or nameof(RawMaterialSplitDetailItem.PalletWeight))
{
RaiseSplitDisplayPropertyChanged();
}
}
/// <summary>
/// 拆码明细某行的「份数」变化时,按公式重算该行「每份重量」:
/// 拆码明细某行的「份数」变化时,仅当该行「每份重量」为空时按公式重算
/// 公式:每份重量 = (总重 - 其他行 Σ份数×每份重量) / 当前行份数
/// — 用户单独手改「每份重量」不触发
/// — 已填写「每份重量」后再改份数,不覆盖用户输入
/// — 总重为空 / ≤0、当前份数 ≤0、剩余总重 ≤0 时跳过;
/// — 结果四舍五入到两位小数
/// — 写入 row.PortionWeight 会触发 PropertyChanged(PortionWeight)不会再次进入本方法PropertyName 已不是 Portions不会形成循环。
/// — 结果四舍五入到两位小数
/// </summary>
private void RecalculatePortionWeightForRow(RawMaterialSplitDetailItem row)
{
if (row.PortionWeight.HasValue)
{
return;
}
if (Entry?.TotalWeight is not { } total || total <= 0d)
{
return;
@@ -898,6 +1160,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
RaisePropertyChanged(nameof(SplitTotalPortionsDisplay));
RaisePropertyChanged(nameof(SplitPortionWeightDisplay));
RaisePropertyChanged(nameof(SplitPortionPackagesDisplay));
RaisePalletTareTotalChanged();
}
private string JoinSplitValue(Func<RawMaterialSplitDetailItem, string?> selector, bool withTrailingSlash)
@@ -938,6 +1201,11 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
Entry.TotalPortions = JoinSplitValue(it => it.Portions?.ToString(CultureInfo.InvariantCulture), true);
Entry.PortionWeight = JoinSplitValue(it => FormatNullableDecimal(it.PortionWeight), true);
Entry.PortionPackagingTare = JoinSplitValue(it => FormatNullableDecimal(it.PackagingTare), true);
Entry.PortionPalletWeight = JoinSplitValue(it => FormatNullableDecimal(it.PalletWeight), true);
Entry.PortionTareStrategyIds = JoinSplitValue(
it => it.IsManualTareStrategy && !string.IsNullOrWhiteSpace(it.TareStrategyId) ? it.TareStrategyId : null,
true);
Entry.PortionPackages = JoinSplitValue(it => it.PortionPackages?.ToString(CultureInfo.InvariantCulture), true);
Entry.PortionWarehouseLocations = JoinSplitValue(it => string.IsNullOrWhiteSpace(it.WarehouseLocation) ? null : it.WarehouseLocation.Trim(), true);
// 持久化每行 GUID行序与上方四个字段对齐JoinSplitValue 会过滤空,但 Id 在构造时即生成不会为空。
@@ -947,6 +1215,7 @@ public class RawMaterialEntryEditDialogViewModel : BaseViewModel, IDialogResulta
// 「生成原材料卡片」只对 HasCard=false 的行加卡 → 必须把真实的 HasCard 持久化,
// 否则重新加载后无法区分「已生成」与「保存后新增的未生成行」。
Entry.PortionCardFlags = JoinSplitValue(it => it.HasCard ? "1" : "0", true);
Entry.PalletTareTotal = PalletTareTotalDisplay;
}
private double CalculateSplitCodeTableHeight()
@@ -983,7 +1252,37 @@ public class RawMaterialSplitDetailItem : BindableBase
public double? PortionWeight
{
get => _portionWeight;
set => SetProperty(ref _portionWeight, value);
set
{
if (SetProperty(ref _portionWeight, value))
{
RaisePropertyChanged(nameof(PortionWeightText));
}
}
}
/// <summary>
/// 每份重量文本(供 TextBox 绑定;空字符串表示未填写,避免 double? 直接绑定无法清空)。
/// </summary>
public string PortionWeightText
{
get => _portionWeight.HasValue
? _portionWeight.Value.ToString("0.##", CultureInfo.InvariantCulture)
: string.Empty;
set
{
var text = value?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(text))
{
PortionWeight = null;
return;
}
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
{
PortionWeight = parsed;
}
}
}
private int? _portionPackages;
@@ -1000,6 +1299,42 @@ public class RawMaterialSplitDetailItem : BindableBase
set => SetProperty(ref _warehouseLocation, value);
}
private double? _packagingTare;
public double? PackagingTare
{
get => _packagingTare;
set => SetProperty(ref _packagingTare, value);
}
private double? _palletWeight;
public double? PalletWeight
{
get => _palletWeight;
set => SetProperty(ref _palletWeight, value);
}
private string? _tareStrategyId;
public string? TareStrategyId
{
get => _tareStrategyId;
set => SetProperty(ref _tareStrategyId, value);
}
private string? _tareStrategyDisplay = "未匹配(0)";
public string? TareStrategyDisplay
{
get => _tareStrategyDisplay;
set => SetProperty(ref _tareStrategyDisplay, value);
}
private bool _isManualTareStrategy;
/// <summary>用户手动选择皮重策略后为 true自动匹配不再覆盖。</summary>
public bool IsManualTareStrategy
{
get => _isManualTareStrategy;
set => SetProperty(ref _isManualTareStrategy, value);
}
/// <summary>
/// 该行是否已生成原材料卡片(运行时状态,不直接持久化)。
/// 加载时根据 Entry.PrintFlag + Id 是否来自持久化的 PortionDetailIds 推断;

View File

@@ -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,

View File

@@ -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;
/// <summary>
/// 拆码明细手动选择「密炼物料皮重策略」弹窗。
/// </summary>
public class TareStrategyPickerDialogViewModel : BaseViewModel, IDialogResultable<bool>
{
private readonly IMixerMaterialTareStrategyService _tareStrategyService;
private string? _materialId;
private string? _supplierId;
private DateTime? _entryDate;
private double? _portionWeight;
public ObservableCollection<MesXslMixerMaterialTareStrategy> 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();
}
}

View File

@@ -0,0 +1,163 @@
<UserControl x:Class="YY.Admin.Views.MixerMaterialTareStrategy.MixerMaterialTareStrategyEditDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
Width="760"
MinHeight="560">
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<hc:SimplePanel Margin="20">
<TextBlock FontSize="18" Foreground="{DynamicResource PrimaryTextBrush}"
Text="{Binding DialogTitle}" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Button Width="22" Height="22" Command="hc:ControlCommands.Close"
Style="{StaticResource ButtonIcon}" Foreground="{DynamicResource PrimaryBrush}"
hc:IconElement.Geometry="{StaticResource ErrorGeometry}"
Padding="0" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,4,4,0"/>
</hc:SimplePanel>
<hc:ScrollViewer Grid.Row="1" IsInertiaEnabled="True">
<StackPanel Margin="20,0,20,0">
<hc:Row Gutter="10">
<hc:Col Span="24">
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<hc:TextBox Grid.Column="0"
Text="{Binding SelectedMaterialDisplay, Mode=OneWay}"
IsReadOnly="True"
hc:InfoElement.Title="密炼物料"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"/>
<Button Grid.Column="1" Margin="8,0,0,0" Content="选择" Width="70"
Command="{Binding OpenMaterialPickerCommand}" Style="{StaticResource ButtonPrimary}"/>
<Button Grid.Column="2" Margin="8,0,0,0" Content="清除" Width="70"
Command="{Binding ClearMaterialCommand}" Style="{StaticResource ButtonDefault}"/>
</Grid>
</hc:Col>
<hc:Col Span="24">
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<hc:TextBox Grid.Column="0"
Text="{Binding SelectedSupplierDisplay, Mode=OneWay}"
IsReadOnly="True"
hc:InfoElement.Title="供应商"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"/>
<Button Grid.Column="1" Margin="8,0,0,0" Content="选择" Width="70"
Command="{Binding OpenSupplierPickerCommand}" Style="{StaticResource ButtonPrimary}"/>
<Button Grid.Column="2" Margin="8,0,0,0" Content="清除" Width="70"
Command="{Binding ClearSupplierCommand}" Style="{StaticResource ButtonDefault}"/>
</Grid>
</hc:Col>
<hc:Col Span="24">
<StackPanel Margin="0,0,0,16">
<hc:TextBox Text="{Binding Strategy.MaterialSpec, UpdateSourceTrigger=PropertyChanged}"
hc:InfoElement.Title="物料规格"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="请输入物料规格"
hc:InfoElement.ShowClearButton="True"
Margin="0,0,0,4"/>
<TextBlock Text="同一供应商/密炼物料下,不同规格可分别维护;规格相同且生效日期重叠时不允许重复"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="12"
TextWrapping="Wrap"
Margin="90,0,0,0"/>
</StackPanel>
</hc:Col>
<hc:Col Span="12">
<hc:NumericUpDown Value="{Binding Strategy.TareWeight}"
Minimum="0"
DecimalPlaces="4"
Style="{StaticResource NumericUpDownPlus}"
hc:InfoElement.Title="包装物重量"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
hc:InfoElement.Placeholder="请输入包装物重量"
Margin="0,0,0,16"/>
</hc:Col>
<hc:Col Span="12">
<hc:NumericUpDown Value="{Binding Strategy.PalletWeight}"
Minimum="0"
DecimalPlaces="4"
Style="{StaticResource NumericUpDownPlus}"
hc:InfoElement.Title="托盘重量"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="请输入托盘重量"
Margin="0,0,0,16"/>
</hc:Col>
<hc:Col Span="12">
<hc:ComboBox SelectedValuePath="Id"
DisplayMemberPath="UnitName"
ItemsSource="{Binding UnitOptions}"
SelectedValue="{Binding Strategy.UnitId, UpdateSourceTrigger=PropertyChanged}"
hc:InfoElement.Title="单位"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
hc:InfoElement.ShowClearButton="True"
Margin="0,0,0,16"/>
</hc:Col>
<hc:Col Span="12">
<hc:DatePicker SelectedDate="{Binding Strategy.EffectiveStartDate, UpdateSourceTrigger=PropertyChanged}"
hc:InfoElement.Title="生效开始"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
Margin="0,0,0,16"/>
</hc:Col>
<hc:Col Span="12">
<hc:DatePicker SelectedDate="{Binding Strategy.EffectiveEndDate, UpdateSourceTrigger=PropertyChanged}"
hc:InfoElement.Title="生效截止"
hc:InfoElement.TitleWidth="90"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Necessary="True"
hc:InfoElement.Symbol="*"
Margin="0,0,0,16"/>
</hc:Col>
</hc:Row>
</StackPanel>
</hc:ScrollViewer>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="20">
<Button Content="取消" Command="{Binding CancelCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,15,0" Width="100"/>
<Button Content="确定" Command="{Binding SaveCommand}" Style="{StaticResource ButtonPrimary}" Width="100"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace YY.Admin.Views.MixerMaterialTareStrategy;
public partial class MixerMaterialTareStrategyEditDialogView : UserControl
{
public MixerMaterialTareStrategyEditDialogView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,138 @@
<UserControl x:Class="YY.Admin.Views.MixerMaterialTareStrategy.MixerMaterialTareStrategyListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<Grid Style="{StaticResource BaseViewStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" CornerRadius="4" Margin="0 0 -10 0">
<hc:Row>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterMixerMaterialName, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="密炼物料"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="65"
hc:InfoElement.Placeholder="请输入密炼物料名称"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
<hc:Col Layout="{hc:ColLayout Xs=12, Sm=8, Md=6, Lg=6, Xl=4}">
<hc:TextBox Text="{Binding FilterSupplierName, UpdateSourceTrigger=PropertyChanged}"
Margin="0 0 10 10"
hc:InfoElement.Title="供应商"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.TitleWidth="65"
hc:InfoElement.Placeholder="请输入供应商名称"
hc:InfoElement.ShowClearButton="True"/>
</hc:Col>
</hc:Row>
</Border>
<Border Grid.Row="1" Margin="0,10">
<hc:UniformSpacingPanel Spacing="10">
<Button Style="{StaticResource ButtonPrimary}" Command="{Binding SearchCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Search"/>
<TextBlock Text="搜索" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonDefault}" Command="{Binding ResetCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Refresh"/>
<TextBlock Text="重置" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonSuccess}" Command="{Binding AddCommand}">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Plus"/>
<TextBlock Text="新增" Style="{StaticResource IconButtonStyle}"/>
</StackPanel>
</Button>
</hc:UniformSpacingPanel>
</Border>
<DataGrid Grid.Row="2"
ItemsSource="{Binding Strategies}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
SelectionMode="Extended"
SelectionUnit="FullRow"
RowHeaderWidth="55"
GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="#FFEDEDED"
VerticalGridLinesBrush="Transparent"
HeadersVisibility="All"
ColumnHeaderStyle="{StaticResource CusDataGridColumnHeaderStyle}"
Style="{StaticResource CusDataGridStyle}"
hc:DataGridAttach.ShowSelectAllButton="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<DataGrid.RowHeaderTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGrid.RowHeaderTemplate>
<DataGrid.Columns>
<DataGridTextColumn Header="密炼物料" Binding="{Binding MixerMaterialName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
<DataGridTextColumn Header="供应商" Binding="{Binding SupplierName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
<DataGridTextColumn Header="物料规格" Binding="{Binding MaterialSpec}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
<DataGridTextColumn Header="包装物重量" Binding="{Binding TareWeight}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="托盘重量" Binding="{Binding PalletWeight}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="单位" Binding="{Binding UnitName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
<DataGridTextColumn Header="生效开始" Binding="{Binding EffectiveStartDateText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="110"/>
<DataGridTextColumn Header="生效截止" Binding="{Binding EffectiveEndDateText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="110"/>
<DataGridTextColumn Header="维护人" Binding="{Binding MaintainBy}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTemplateColumn Header="操作" Width="140" CellStyle="{StaticResource CusOperDataGridCellStyle}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<hc:UniformSpacingPanel Spacing="5">
<Border Style="{DynamicResource DataGridOpeButtonStyle}">
<Border.InputBindings>
<MouseBinding Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}" MouseAction="LeftClick"/>
</Border.InputBindings>
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="SquareEditOutline" VerticalAlignment="Center"/>
<TextBlock Text="修改" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border Style="{DynamicResource DataGridOpeButtonStyle}">
<Border.InputBindings>
<MouseBinding Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding}" MouseAction="LeftClick"/>
</Border.InputBindings>
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="TrashCanOutline" VerticalAlignment="Center"/>
<TextBlock Text="删除" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</hc:UniformSpacingPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<TextBlock Text="{Binding Total, StringFormat=共 {0} 条}" VerticalAlignment="Center" Margin="0,0,16,0"
Foreground="{DynamicResource SecondaryTextBrush}"/>
<Button Content="上一页" Command="{Binding PrevPageCommand}" Style="{StaticResource ButtonDefault}" Margin="0,0,4,0" Width="80"/>
<TextBlock Text="{Binding PageNo, StringFormat=第 {0} 页}" VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<Button Content="下一页" Command="{Binding NextPageCommand}" Style="{StaticResource ButtonDefault}" Width="80"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace YY.Admin.Views.MixerMaterialTareStrategy;
public partial class MixerMaterialTareStrategyListView : UserControl
{
public MixerMaterialTareStrategyListView()
{
InitializeComponent();
}
}

View File

@@ -467,20 +467,20 @@ public partial class PrintPreviewWindow : HandyControl.Controls.Window
{
if (_printDotService == null)
{
HandyControl.Controls.Growl.Warning("PrintDot 服务不可用");
HandyControl.Controls.MessageBox.Warning("PrintDot 服务不可用");
return;
}
if (string.IsNullOrWhiteSpace(_templateJson) || _templateJson == "{}")
{
HandyControl.Controls.Growl.Warning("当前模板没有可打印内容");
HandyControl.Controls.MessageBox.Warning("当前模板没有可打印内容");
return;
}
var selected = PrinterCombo.SelectedItem as PrintDotPrinter;
if (selected == null || string.IsNullOrWhiteSpace(selected.Name))
{
HandyControl.Controls.Growl.Warning("请先选择打印机");
HandyControl.Controls.MessageBox.Warning("请先选择打印机");
return;
}
@@ -521,13 +521,13 @@ public partial class PrintPreviewWindow : HandyControl.Controls.Window
await _printDotService.PrintAsync(selected.Name, pdfBase64, jobName, copies: 1);
SetStatus("打印任务已发送");
HandyControl.Controls.Growl.Success("打印任务已发送至 PrintDot");
HandyControl.Controls.MessageBox.Success("打印任务已发送至 PrintDot");
}
catch (Exception ex)
{
// 顶部状态栏单行显示首行,完整多行处理步骤在 Growl 弹窗中展示
// 独立预览窗无 Growl 面板,用状态栏 + MessageBox 展示错误,避免 NullReferenceException
SetStatus($"打印失败:{ex.Message}");
HandyControl.Controls.Growl.Error($"打印失败:{ex.Message}");
HandyControl.Controls.MessageBox.Error($"打印失败:{ex.Message}");
}
finally
{

View File

@@ -127,6 +127,8 @@
<DataGridTextColumn Header="厂家物料名称" Binding="{Binding ManufacturerMaterialName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
<DataGridTextColumn Header="保质期" Binding="{Binding ShelfLife}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="总重(kg)" Binding="{Binding TotalWeight, StringFormat=N3}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="包装物皮重" Binding="{Binding PackagingTare, StringFormat=N3}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="95"/>
<DataGridTextColumn Header="托盘重量" Binding="{Binding PalletWeight, StringFormat=N3}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="剩余重量" Binding="{Binding RemainingWeight, StringFormat=N3}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="剩余数量" Binding="{Binding RemainingQuantity}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="80"/>
<DataGridTextColumn Header="状态" Binding="{Binding StatusText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="70"/>

View File

@@ -124,6 +124,7 @@
<DataGridTextColumn Header="供料客户" Binding="{Binding SupplyCustomer}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
<DataGridTextColumn Header="供应商名称" Binding="{Binding SupplierName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="140"/>
<DataGridTextColumn Header="总重(KG)" Binding="{Binding TotalWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="托盘及皮重(合计)" Binding="{Binding PalletTareTotal, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
<DataGridTextColumn Header="总份数" Binding="{Binding TotalPortions}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="70"/>
<DataGridTextColumn Header="检测结果" Binding="{Binding TestResultText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="85"/>
<DataGridTextColumn Header="检测状态" Binding="{Binding TestStatusText}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="85"/>

View File

@@ -132,6 +132,7 @@
<RowDefinition Height="36"/> <!-- 厂家物料名称 / 保质期 -->
<RowDefinition Height="40"/> <!-- 总重 / 总份数 — 多 4px 给 NumericUpDown 上下间距,否则 spinner 箭头会被裁切 -->
<RowDefinition Height="36"/> <!-- 每份总重 / 每份包数 -->
<RowDefinition Height="36"/> <!-- 托盘及皮重(合计) -->
<RowDefinition Height="36"/> <!-- 库位 / 卸货人 -->
<RowDefinition Height="60"/> <!-- 备注 -->
</Grid.RowDefinitions>
@@ -205,7 +206,7 @@
<Border Grid.Row="1" Grid.Column="3"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="4,3">
<!-- 自定义日期时间选择器:日历 + 时/分/秒 三列 + 此刻/确定 -->
<ctrls:DateTimeListPicker SelectedDateTime="{Binding Entry.EntryTime, Mode=TwoWay}"
<ctrls:DateTimeListPicker SelectedDateTime="{Binding EntryTimeInput, Mode=TwoWay}"
DateTimeFormat="yyyy-MM-dd HH:mm:ss"
Placeholder="请选择入场时间"/>
</Border>
@@ -458,14 +459,30 @@
</Grid>
</Border>
<!-- ===== Row 7: 库位 / 卸货人 ===== -->
<!-- ===== Row 7: 托盘及皮重(合计) ===== -->
<Border Grid.Row="7" Grid.Column="0"
Background="{DynamicResource SecondaryRegionBrush}"
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}">
<TextBlock Text="托盘及皮重(合计)" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="11" Foreground="{DynamicResource PrimaryTextBrush}"
TextWrapping="Wrap" TextAlignment="Center"/>
</Border>
<Border Grid.Row="7" Grid.Column="1" Grid.ColumnSpan="3"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="6,0">
<TextBlock Text="{Binding PalletTareTotalDisplay, StringFormat={}{0:0.##}}"
VerticalAlignment="Center" FontSize="13"
Foreground="{DynamicResource SecondaryTextBrush}"
ToolTip="由拆码明细自动汇总:Σ份数×(包装物皮重+托盘重量)"/>
</Border>
<!-- ===== Row 8: 库位 / 卸货人 ===== -->
<Border Grid.Row="8" Grid.Column="0"
Background="{DynamicResource SecondaryRegionBrush}"
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}">
<TextBlock Text="库位" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="12" Foreground="{DynamicResource PrimaryTextBrush}"/>
</Border>
<Border Grid.Row="7" Grid.Column="1"
<Border Grid.Row="8" Grid.Column="1"
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}" Padding="4,0">
<Grid>
<TextBox Text="{Binding Entry.WarehouseLocation, UpdateSourceTrigger=PropertyChanged}"
@@ -490,13 +507,13 @@
</TextBlock>
</Grid>
</Border>
<Border Grid.Row="7" Grid.Column="2"
<Border Grid.Row="8" Grid.Column="2"
Background="{DynamicResource SecondaryRegionBrush}"
BorderThickness="0,0,1,1" BorderBrush="{DynamicResource BorderBrush}">
<TextBlock Text="卸货人" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="12" Foreground="{DynamicResource PrimaryTextBrush}"/>
</Border>
<Border Grid.Row="7" Grid.Column="3"
<Border Grid.Row="8" Grid.Column="3"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BorderBrush}" Padding="4,0">
<Grid>
<TextBox Text="{Binding Entry.UnloadOperator, UpdateSourceTrigger=PropertyChanged}"
@@ -522,14 +539,14 @@
</Grid>
</Border>
<!-- ===== Row 8: 备注(末行,无内部底边线) ===== -->
<Border Grid.Row="8" Grid.Column="0"
<!-- ===== Row 9: 备注(末行,无内部底边线) ===== -->
<Border Grid.Row="9" Grid.Column="0"
Background="{DynamicResource SecondaryRegionBrush}"
BorderThickness="0,0,1,0" BorderBrush="{DynamicResource BorderBrush}">
<TextBlock Text="备注" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="12" Foreground="{DynamicResource PrimaryTextBrush}"/>
</Border>
<Border Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="3" Padding="4,4">
<Border Grid.Row="9" Grid.Column="1" Grid.ColumnSpan="3" Padding="4,4">
<Grid>
<TextBox Text="{Binding Entry.Remark, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap" AcceptsReturn="True"
@@ -644,42 +661,54 @@
</StackPanel>
</DockPanel>
<StackPanel>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel MinWidth="820">
<!-- 表头 -->
<Grid Background="{DynamicResource SecondaryRegionBrush}" Height="40">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="份数"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="13"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<TextBlock Grid.Column="1" Text="每份重量(KG)"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="13"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<TextBlock Grid.Column="2" Text="每份包数"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="13"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<!-- 库位列保存时非必填点击「生成原材料卡片」时必填ToolTip 给出说明 -->
<TextBlock Grid.Column="3" Text="库位"
<TextBlock Grid.Column="3" Text="包装物皮重"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="13"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"
ToolTip="自动匹配皮重策略;点击可手动选择"/>
<TextBlock Grid.Column="4" Text="托盘重量"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"
ToolTip="由皮重策略自动匹配未匹配时为0"/>
<TextBlock Grid.Column="5" Text="库位"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"
ToolTip="生成原材料卡片时为必填项"/>
<TextBlock Grid.Column="4" Text="打印标记"
<TextBlock Grid.Column="6" Text="打印标记"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="13"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<TextBlock Grid.Column="5" Text="操作"
<TextBlock Grid.Column="7" Text="操作"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontSize="13"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource PrimaryTextBrush}"/>
</Grid>
@@ -736,19 +765,21 @@
</DataTemplate.Resources>
<Grid Height="44">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="90"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<TextBox Text="{Binding Portions, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource LockableSplitTextBoxStyle}"/>
</Border>
<Border Grid.Column="1" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<TextBox Text="{Binding PortionWeight, UpdateSourceTrigger=PropertyChanged}"
<TextBox Text="{Binding PortionWeightText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Style="{StaticResource LockableSplitTextBoxStyle}"/>
</Border>
<Border Grid.Column="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
@@ -756,6 +787,59 @@
Style="{StaticResource LockableSplitTextBoxStyle}"/>
</Border>
<Border Grid.Column="3" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<Button Command="{Binding DataContext.OpenTareStrategyPickerCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
ToolTip="{Binding TareStrategyDisplay}"
Cursor="Hand"
VerticalAlignment="Center"
Height="32"
Margin="4,0"
Focusable="False">
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="True"/>
<Setter Property="ToolTip" Value="点击选择皮重策略"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasCard}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
<Setter Property="ToolTip" Value="该行已生成原材料卡片,不可修改。如需调整请先点「重新拆码」清空全部卡片"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
BorderBrush="#D9D9D9" BorderThickness="1"
CornerRadius="2"
Background="White">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="13"
Text="{Binding PackagingTare, StringFormat={}{0:0.##}}"
Foreground="{DynamicResource PrimaryTextBrush}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="#4096FF"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#F0F7FF"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Background" Value="#F5F5F5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
<Border Grid.Column="4" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<TextBlock Text="{Binding PalletWeight, StringFormat={}{0:0.##}}"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="12" Foreground="{DynamicResource SecondaryTextBrush}"
ToolTip="{Binding TareStrategyDisplay}"/>
</Border>
<Border Grid.Column="5" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<!--
InputBindings 内 RelativeSource 不在可视树中,查找会静默失败。
改用 Button + ControlTemplateCommand 写在 Button 元素上可视树正常RelativeSource 可靠。
@@ -836,11 +920,7 @@
</Button.Template>
</Button>
</Border>
<!--
打印标记列行级状态HasCard与「继续拆码」流程契合。
旧行(已生成卡片)显示绿色「已打印」;新增的待生成行显示灰色「未打印」。
-->
<Border Grid.Column="4" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<Border Grid.Column="6" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<Grid>
<Border CornerRadius="2" Padding="6,2" VerticalAlignment="Center" HorizontalAlignment="Center">
<Border.Style>
@@ -873,7 +953,7 @@
</Border>
</Grid>
</Border>
<Border Grid.Column="5" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<Border Grid.Column="7" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<!-- 行级HasCard==true该行已生成卡片时隐藏删除按钮新增的待生成行可正常删除 -->
<Button Content="删除"
Command="{Binding DataContext.RemoveSplitDetailCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
@@ -897,6 +977,7 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</Border>
</StackPanel>

View File

@@ -0,0 +1,48 @@
<UserControl x:Class="YY.Admin.Views.RawMaterialEntry.TareStrategyPickerDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Width="820" MinHeight="420">
<Grid Background="{DynamicResource ThirdlyRegionBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Margin="20,16,20,8" Text="选择皮重策略"
FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource PrimaryTextBrush}"/>
<DataGrid Grid.Row="1" Margin="16,0"
ItemsSource="{Binding Records}"
SelectedItem="{Binding SelectedStrategy}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="物料规格" Binding="{Binding MaterialSpec}" Width="100"/>
<DataGridTextColumn Header="包装物重量" Binding="{Binding TareWeight}" Width="100"/>
<DataGridTextColumn Header="托盘重量" Binding="{Binding PalletWeight}" Width="100"/>
<DataGridTextColumn Header="单位" Binding="{Binding UnitName}" Width="80"/>
<DataGridTextColumn Header="生效开始" Binding="{Binding EffectiveStartDate, StringFormat=yyyy-MM-dd}" Width="110"/>
<DataGridTextColumn Header="生效截止" Binding="{Binding EffectiveEndDate, StringFormat=yyyy-MM-dd}" Width="110"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="2" Margin="16,12">
<TextBlock Text="{Binding SelectedStrategyDisplay}"
Foreground="{DynamicResource SecondaryTextBrush}"
TextWrapping="Wrap" Margin="0,0,0,10"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="取消" Command="{Binding CancelCommand}" Style="{StaticResource ButtonDefault}" Width="100" Margin="0,0,10,0"/>
<Button Content="使用自动匹配" Command="{Binding UseAutoMatchCommand}" Style="{StaticResource ButtonDefault}" Width="120" Margin="0,0,10,0"/>
<Button Content="确认选择" Command="{Binding ConfirmCommand}" Style="{StaticResource ButtonPrimary}" Width="100"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,9 @@
namespace YY.Admin.Views.RawMaterialEntry;
public partial class TareStrategyPickerDialogView
{
public TareStrategyPickerDialogView()
{
InitializeComponent();
}
}

View File

@@ -140,6 +140,7 @@
<DataGridTextColumn Header="供应商(发货单位)" Binding="{Binding SenderUnit}" Width="*"/>
<DataGridTextColumn Header="净重(KG)" Binding="{Binding NetWeight, StringFormat=N2}" Width="90"/>
<DataGridTextColumn Header="已入场(KG)" Binding="{Binding EnteredWeight, StringFormat=N2}" Width="100"/>
<DataGridTextColumn Header="货物皮重" Binding="{Binding CargoTareWeight, StringFormat=N2}" Width="90"/>
<DataGridTextColumn Header="称重日期" Binding="{Binding WeighDate, StringFormat='yyyy-MM-dd'}" Width="110"/>
</DataGrid.Columns>
</DataGrid>

View File

@@ -148,6 +148,8 @@
<DataGridTextColumn Header="皮重(KG)" Binding="{Binding TareWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="净重(KG)" Binding="{Binding NetWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="已入场重量(KG)" Binding="{Binding EnteredWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>
<DataGridTextColumn Header="货物皮重" Binding="{Binding CargoTareWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="原料重量" Binding="{Binding RawMaterialWeight, StringFormat=N2}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="100"/>
<DataGridTextColumn Header="司机" Binding="{Binding DriverName}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="90"/>
<DataGridTextColumn Header="手机号" Binding="{Binding DriverPhone}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="120"/>
<DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime, StringFormat='yyyy-MM-dd HH:mm'}" CellStyle="{StaticResource CusDataGridCellStyle}" Width="130"/>

View File

@@ -179,12 +179,54 @@ WHERE NOT EXISTS (
grantLoginSettingsRole.ExecuteNonQuery();
}
// 基础资料下「密炼物料皮重策略」菜单
long tareStrategyMenuId = 1300150011101;
var ensureTareStrategy = conn.CreateCommand();
ensureTareStrategy.Transaction = tx;
ensureTareStrategy.CommandText = @"
INSERT INTO sys_menu (id, pid, type, name, path, component, title, icon, is_iframe, is_hide, is_keep_alive, is_affix, order_no, status, create_time)
SELECT $id, $pid, 2, 'mesXslMixerMaterialTareStrategy', '/xslmes/mesXslMixerMaterialTareStrategy', 'MixerMaterialTareStrategyListView', '密炼物料皮重策略', '&#xe7ce;', 0, 0, 1, 0, 110, 1, datetime('now')
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = $id OR name = 'mesXslMixerMaterialTareStrategy');
";
ensureTareStrategy.Parameters.AddWithValue("$id", tareStrategyMenuId);
ensureTareStrategy.Parameters.AddWithValue("$pid", baseMenuId);
ensureTareStrategy.ExecuteNonQuery();
var grantTareStrategyTenant = conn.CreateCommand();
grantTareStrategyTenant.Transaction = tx;
grantTareStrategyTenant.CommandText = @"
INSERT INTO sys_tenant_menu (tenant_id, menu_id)
SELECT 1300000000001, $menuId
WHERE NOT EXISTS (
SELECT 1 FROM sys_tenant_menu t WHERE t.tenant_id = 1300000000001 AND t.menu_id = $menuId
);
";
grantTareStrategyTenant.Parameters.AddWithValue("$menuId", tareStrategyMenuId);
grantTareStrategyTenant.ExecuteNonQuery();
if (roleObj != null && roleObj != DBNull.Value)
{
var adminRoleId = Convert.ToInt64(roleObj);
var grantTareStrategyRole = conn.CreateCommand();
grantTareStrategyRole.Transaction = tx;
grantTareStrategyRole.CommandText = @"
INSERT INTO sys_role_menu (id, role_id, menu_id)
SELECT (SELECT IFNULL(MAX(id),0)+1 FROM sys_role_menu), $roleId, $menuId
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_menu r WHERE r.role_id = $roleId AND r.menu_id = $menuId
);
";
grantTareStrategyRole.Parameters.AddWithValue("$roleId", adminRoleId);
grantTareStrategyRole.Parameters.AddWithValue("$menuId", tareStrategyMenuId);
grantTareStrategyRole.ExecuteNonQuery();
}
var verify = conn.CreateCommand();
verify.Transaction = tx;
verify.CommandText = @"
SELECT id, pid, title, path, name, component, type, status, order_no
FROM sys_menu
WHERE title IN ('基础资料','车辆管理','客户管理')
WHERE title IN ('基础资料','车辆管理','客户管理','密炼物料皮重策略')
ORDER BY title, id;
";
using var reader = verify.ExecuteReader();

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
using Microsoft.Data.Sqlite;
var dbPath = args[0];
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();
using var tx = conn.BeginTransaction();
void Exec(string sql) {
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = sql;
Console.WriteLine($"{sql.Split('\n')[0].Trim()} => {cmd.ExecuteNonQuery()}");
}
Exec(@"INSERT INTO sys_menu (id, pid, type, name, path, component, title, icon, is_iframe, is_hide, is_keep_alive, is_affix, order_no, status, create_time)
SELECT 1300150011101, 1300150000101, 2, 'mesXslMixerMaterialTareStrategy', '/xslmes/mesXslMixerMaterialTareStrategy', 'MixerMaterialTareStrategyListView', '密炼物料皮重策略', '&#xe7ce;', 0, 0, 1, 0, 110, 1, datetime('now')
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 1300150011101 OR name = 'mesXslMixerMaterialTareStrategy');");
Exec(@"INSERT INTO sys_tenant_menu (id, tenant_id, menu_id)
SELECT 1300150011101, 1300000000001, 1300150011101
WHERE NOT EXISTS (SELECT 1 FROM sys_tenant_menu WHERE tenant_id = 1300000000001 AND menu_id = 1300150011101);");
Exec(@"INSERT INTO sys_role_menu (id, role_id, menu_id)
SELECT (SELECT IFNULL(MAX(id),0)+1 FROM sys_role_menu),
(SELECT id FROM sys_role ORDER BY id LIMIT 1),
1300150011101
WHERE NOT EXISTS (
SELECT 1 FROM sys_role_menu rm
WHERE rm.role_id = (SELECT id FROM sys_role ORDER BY id LIMIT 1) AND rm.menu_id = 1300150011101
);");
tx.Commit();
using var q = conn.CreateCommand();
q.CommandText = "SELECT id, pid, title, name, status FROM sys_menu WHERE name='mesXslMixerMaterialTareStrategy';";
using var r = q.ExecuteReader();
while (r.Read()) Console.WriteLine($"OK: id={r.GetInt64(0)} pid={r.GetInt64(1)} title={r.GetString(2)} status={r.GetInt32(4)}");

View File

@@ -0,0 +1,50 @@
# 为已有桌面端 SQLite 库补「密炼物料皮重策略」菜单(幂等)
$dbPath = Join-Path $env:LOCALAPPDATA "YY.Admin\Data\Admin.NET.db"
if (-not (Test-Path $dbPath)) {
Write-Host "未找到数据库: $dbPath"
exit 1
}
$proj = Join-Path $PSScriptRoot "MenuFixTemp"
New-Item -ItemType Directory -Force -Path $proj | Out-Null
@'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
</ItemGroup>
</Project>
'@ | Set-Content "$proj\MenuFixTemp.csproj" -Encoding UTF8
@'
using Microsoft.Data.Sqlite;
var dbPath = args[0];
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();
using var tx = conn.BeginTransaction();
void Exec(string sql) {
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
Exec(@"INSERT INTO sys_menu (id, pid, type, name, path, component, title, icon, is_iframe, is_hide, is_keep_alive, is_affix, order_no, status, create_time)
SELECT 1300150011101, 1300150000101, 2, 'mesXslMixerMaterialTareStrategy', '/xslmes/mesXslMixerMaterialTareStrategy', 'MixerMaterialTareStrategyListView', '密炼物料皮重策略', '&#xe7ce;', 0, 0, 1, 0, 110, 1, datetime('now')
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE id = 1300150011101 OR name = 'mesXslMixerMaterialTareStrategy');");
Exec(@"INSERT INTO sys_tenant_menu (id, tenant_id, menu_id)
SELECT 1300150011101, 1300000000001, 1300150011101
WHERE NOT EXISTS (SELECT 1 FROM sys_tenant_menu WHERE tenant_id = 1300000000001 AND menu_id = 1300150011101);");
Exec(@"INSERT INTO sys_role_menu (id, role_id, menu_id)
SELECT (SELECT IFNULL(MAX(id),0)+1 FROM sys_role_menu), (SELECT id FROM sys_role ORDER BY id LIMIT 1), 1300150011101
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = (SELECT id FROM sys_role ORDER BY id LIMIT 1) AND rm.menu_id = 1300150011101);");
tx.Commit();
Console.WriteLine("菜单补全完成");
'@ | Set-Content "$proj\Program.cs" -Encoding UTF8
dotnet run --project "$proj\MenuFixTemp.csproj" -- "$dbPath"