Jelajahi Sumber

fix: 打通生产订单-生产计划-工件关联链路

hbx 6 hari lalu
induk
melakukan
a7167fc8a3

+ 92 - 0
db/mes_product_plan_link_fix.sql

@@ -0,0 +1,92 @@
+-- 订单-计划-产品关联修复(本地/测试库需执行)
+-- 背景:
+--   1) mes_product_plan.shift_id 为 char(1),但 mes_shift.id 为 varchar(64) 的雪花ID,
+--      导致计划保存真实班次ID时报 “Data too long”,getCurPlan 也永远匹配不到班次。
+--   2) 部分环境缺少代码已使用的字段/表,导致创建件、打包、按计划领料接口 500。
+-- 说明:本脚本只做结构补齐,不自动改写历史 shift_id=1/2 的业务数据。
+--       历史计划需要按实际班次人工确认后再迁移。
+
+-- 1. 计划表班次字段扩宽,保存真实 mes_shift.id
+ALTER TABLE `mes_product_plan`
+  MODIFY COLUMN `shift_id` VARCHAR(64) NULL COMMENT '班次';
+
+-- 2. 产品型号表补 cate_sn(MesProductModel 实体已映射)
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_model' AND COLUMN_NAME = 'cate_sn');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_model` ADD COLUMN `cate_sn` VARCHAR(50) DEFAULT NULL COMMENT ''工件类型'' AFTER `state`',
+  'SELECT ''mes_product_model.cate_sn 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- 3. 后面板记录表补 line_sn(MesProductHmb 实体已映射)
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_hmb' AND COLUMN_NAME = 'line_sn');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_hmb` ADD COLUMN `line_sn` VARCHAR(100) DEFAULT NULL COMMENT ''产线编号'' AFTER `state`',
+  'SELECT ''mes_product_hmb.line_sn 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- 4. 领料单表补 plan_id(WmsPick 实体已映射,按计划领料需要)
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wms_pick' AND COLUMN_NAME = 'plan_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `wms_pick` ADD COLUMN `plan_id` VARCHAR(64) DEFAULT NULL COMMENT ''生产计划'' AFTER `order_id`',
+  'SELECT ''wms_pick.plan_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- 5. BOM 表补 product_cate_id(生产计划按工件类型匹配 BOM 使用)
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wms_bom' AND COLUMN_NAME = 'product_cate_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `wms_bom` ADD COLUMN `product_cate_id` VARCHAR(50) DEFAULT NULL COMMENT ''工件类型''',
+  'SELECT ''wms_bom.product_cate_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- 6. 打码规则配置表(MesProductPreConfig 实体已使用)
+CREATE TABLE IF NOT EXISTS `mes_product_pre_config` (
+  `id` VARCHAR(64) NOT NULL COMMENT 'id',
+  `title` VARCHAR(64) DEFAULT NULL COMMENT '标题',
+  `cate` CHAR(1) DEFAULT NULL COMMENT '类型',
+  `line_sn` VARCHAR(64) DEFAULT NULL COMMENT '产线编号',
+  `start_date` DATETIME DEFAULT NULL COMMENT '开始时间',
+  `end_date` DATETIME DEFAULT NULL COMMENT '结束时间',
+  `content` TEXT COMMENT '规则',
+  `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
+  `status` CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1删除 2停用)',
+  `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人',
+  `create_date` DATETIME DEFAULT NULL COMMENT '创建日期',
+  `update_by` VARCHAR(64) DEFAULT NULL COMMENT '修改人',
+  `update_date` DATETIME DEFAULT NULL COMMENT '修改日期',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='打码规则配置信息';
+
+-- 6. 关联查询索引(按需保留)
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_plan' AND INDEX_NAME = 'idx_mpp_day_shift_status');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpp_day_shift_status` ON `mes_product_plan` (`day`, `shift_id`, `status`)',
+  'SELECT ''idx_mpp_day_shift_status 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product' AND INDEX_NAME = 'idx_mp_plan_id');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mp_plan_id` ON `mes_product` (`plan_id`)',
+  'SELECT ''idx_mp_plan_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- 7. 历史数据提示(默认不执行,请确认班次名称唯一后人工执行)
+-- 旧计划中的 shift_id=1/2 是历史占位值,不会自动映射,避免误改。
+-- UPDATE mes_product_plan p
+--   JOIN mes_shift s ON s.title = '白班'
+--   SET p.shift_id = s.id
+--   WHERE p.shift_id = '1';
+-- UPDATE mes_product_plan p
+--   JOIN mes_shift s ON s.title = '晚班'
+--   SET p.shift_id = s.id
+--   WHERE p.shift_id = '2';
+--
+-- 另外,生产订单关联的 BOM 必须维护 wms_bom.product_cate_id,
+-- 否则该订单无法排产。请按实际产品类型补全后再上线。
+
+SELECT '订单-计划-产品关联修复脚本执行完成' AS message;

+ 9 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductBb.java

@@ -36,6 +36,7 @@ public class MesProductBb extends DataEntity<MesProductBb> {
 	private Date day;		// 日期
 	private Date dayEnd;    //结束日期
 	private String shiftId;		// 班次
+	private String shiftTitle;	// 班次名称
 	private String nums;		// 计划数量
 	private String lineOff;		// 下线数量
 	private String repair;		// 返修数量
@@ -75,6 +76,14 @@ public class MesProductBb extends DataEntity<MesProductBb> {
 		this.shiftId = shiftId;
 	}
 
+	public String getShiftTitle() {
+		return shiftTitle;
+	}
+
+	public void setShiftTitle(String shiftTitle) {
+		this.shiftTitle = shiftTitle;
+	}
+
 	@Size(min=0, max=64, message="计划数量长度不能超过 64 个字符")
 	public String getNums() {
 		return nums;

+ 10 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductPlan.java

@@ -46,6 +46,7 @@ import com.jeesite.modules.wms.entity.WmsSupplier;
 				on="d.id = c.bom_id",
 				columns={
 						@Column(name="title", attrName="bomTitle", label="BOM名称", queryType=QueryType.LIKE),
+						@Column(name="product_cate_id", attrName="bomProductCateId", label="工件类型", queryType=QueryType.EQ),
 				}),
 }, orderBy="a.update_date DESC"
 )
@@ -57,6 +58,7 @@ public class MesProductPlan extends DataEntity<MesProductPlan> {
 	private Date day;		// 日期
 	private String shiftId;		// 班次
 	private String bomTitle;
+	private String bomProductCateId;	// BOM对应的工件类型
 	private Integer nums;		// 生产数量
 	private String orderSn;
 	private String shiftTitle;
@@ -138,6 +140,14 @@ public class MesProductPlan extends DataEntity<MesProductPlan> {
 		this.bomTitle = bomTitle;
 	}
 
+	public String getBomProductCateId() {
+		return bomProductCateId;
+	}
+
+	public void setBomProductCateId(String bomProductCateId) {
+		this.bomProductCateId = bomProductCateId;
+	}
+
 	public Integer getNums2() {
 		return nums2;
 	}

+ 133 - 17
src/main/java/com/jeesite/modules/mes/service/MesProductPlanService.java

@@ -1,12 +1,12 @@
 package com.jeesite.modules.mes.service;
 
-import java.text.SimpleDateFormat;
-import java.util.Date;
 import java.util.List;
 
-import com.jeesite.common.lang.ObjectUtils;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.service.ServiceException;
 import com.jeesite.modules.mes.entity.MesProduct;
+import com.jeesite.modules.mes.entity.MesProductOrder;
 import com.jeesite.modules.mes.entity.MesShift;
 import com.jeesite.modules.mes.resp.MesShiftDataResp;
 import org.springframework.stereotype.Service;
@@ -16,6 +16,8 @@ import com.jeesite.common.entity.Page;
 import com.jeesite.common.service.CrudService;
 import com.jeesite.modules.mes.entity.MesProductPlan;
 import com.jeesite.modules.mes.dao.MesProductPlanDao;
+import com.jeesite.modules.wms.entity.WmsBom;
+import com.jeesite.modules.wms.service.WmsBomService;
 
 /**
  * 生产计划Service
@@ -27,10 +29,15 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 
 	private final MesShiftService mesShiftService;
 	private final MesProductService mesProductService;
+	private final MesProductOrderService mesProductOrderService;
+	private final WmsBomService wmsBomService;
 
-	public MesProductPlanService(MesShiftService mesShiftService, MesProductService mesProductService) {
+	public MesProductPlanService(MesShiftService mesShiftService, MesProductService mesProductService,
+			MesProductOrderService mesProductOrderService, WmsBomService wmsBomService) {
 		this.mesShiftService = mesShiftService;
 		this.mesProductService = mesProductService;
+		this.mesProductOrderService = mesProductOrderService;
+		this.wmsBomService = wmsBomService;
 	}
 
 	/**
@@ -76,6 +83,68 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 	@Transactional
 	public void save(MesProductPlan mesProductPlan) {
 		if(mesProductPlan.getIsNewRecord()){
+			if(StringUtils.isBlank(mesProductPlan.getOrderId())
+					|| mesProductPlan.getDay() == null
+					|| StringUtils.isBlank(mesProductPlan.getShiftId())
+					|| mesProductPlan.getNums() == null
+					|| mesProductPlan.getNums() <= 0){
+				throw new ServiceException(text("生产订单、日期、班次、生产数量不能为空,且生产数量必须大于0"));
+			}
+
+			MesShift mesShift = mesShiftService.get(new MesShift(mesProductPlan.getShiftId()));
+			if(mesShift == null || !"0".equals(mesShift.getStatus())){
+				throw new ServiceException(text("所选班次不存在或已停用,请重新选择"));
+			}
+
+			MesProductOrder mesProductOrder = mesProductOrderService.get(new MesProductOrder(mesProductPlan.getOrderId()));
+			if(mesProductOrder == null || !"1".equals(mesProductOrder.getState())){
+				throw new ServiceException(text("只有已下发的生产订单才能排产"));
+			}
+
+			String bomCateSn = null;
+			if(StringUtils.isNotBlank(mesProductOrder.getBomId())){
+				WmsBom wmsBom = wmsBomService.get(new WmsBom(mesProductOrder.getBomId()));
+				if(wmsBom != null){
+					bomCateSn = wmsBom.getProductCateId();
+				}
+			}
+			if(StringUtils.isBlank(bomCateSn)){
+				throw new ServiceException(text("生产订单的BOM未维护工件类型,无法排产"));
+			}
+
+			MesProductPlan query = new MesProductPlan();
+			query.setOrderId(mesProductPlan.getOrderId());
+			query.setDay(mesProductPlan.getDay());
+			query.setShiftId(mesProductPlan.getShiftId());
+			query.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+			if(!findList(query).isEmpty()){
+				throw new ServiceException(text("同一订单、日期、班次已存在有效的生产计划,不能重复排产"));
+			}
+
+			// 同一日期、班次、产品只能有一个计划,否则产品创建时无法唯一确定计划。
+			MesProductPlan slotQuery = new MesProductPlan();
+			slotQuery.setDay(mesProductPlan.getDay());
+			slotQuery.setShiftId(mesProductPlan.getShiftId());
+			slotQuery.setBomProductCateId(bomCateSn);
+			slotQuery.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+			if(!findList(slotQuery).isEmpty()){
+				throw new ServiceException(text("同一日期、班次、产品已存在生产计划,不能重复排产"));
+			}
+
+			MesProductPlan sumQuery = new MesProductPlan();
+			sumQuery.setOrderId(mesProductPlan.getOrderId());
+			sumQuery.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+			long plannedNums = 0L;
+			for(MesProductPlan plan : findList(sumQuery)){
+				if(plan.getNums() != null){
+					plannedNums += plan.getNums();
+				}
+			}
+			if(mesProductOrder.getNums() != null
+					&& plannedNums + mesProductPlan.getNums() > mesProductOrder.getNums()){
+				throw new ServiceException(text("累计计划数量不能超过生产订单数量"));
+			}
+
 			mesProductPlan.setIsPick("0");
 		}
 		super.save(mesProductPlan);
@@ -98,24 +167,71 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 	@Override
 	@Transactional
 	public void delete(MesProductPlan mesProductPlan) {
+		if(mesProductPlan != null && StringUtils.isNotBlank(mesProductPlan.getId())){
+			MesProduct query = new MesProduct();
+			query.setPlanId(mesProductPlan.getId());
+			if(mesProductService.findCount(query) > 0){
+				throw new ServiceException(text("该计划已绑定产品,不能直接删除"));
+			}
+		}
 		super.delete(mesProductPlan);
 	}
 
-	public MesProductPlan getCurPlan(){
-		MesProductPlan mesProductPlan1 = null;
+	/**
+	 * 获取当前班次、指定工件类型对应的唯一有效生产计划。
+	 *
+	 * 计划日期以“班次开始日期”为准,因此跨零点的夜班在次日凌晨仍然匹配前一天的计划。
+	 *
+	 * @param cateSn 产品工件类型(对应 wms_bom.product_cate_id)
+	 * @return 唯一计划;没有计划时返回 null
+	 */
+	public MesProductPlan getCurrentPlan(String cateSn){
+		if(StringUtils.isBlank(cateSn)){
+			throw new ServiceException(text("产品未维护工件类型,无法匹配生产计划"));
+		}
+
 		MesShiftDataResp mesShiftDataResp = mesShiftService.getCurShift();
-		if(!ObjectUtils.isEmpty(mesShiftDataResp)){
-			MesProductPlan mesProductPlan = new MesProductPlan();
-			Date date = new Date();
-			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-			String timeStr = sdf.format(date);
-
-			mesProductPlan.getSqlMap().getWhere().and("a.day", QueryType.EQ, timeStr);
-			mesProductPlan.setShiftId(mesShiftDataResp.getMesShift().getId());
-			mesProductPlan1 = findInfo(mesProductPlan);
+		if(mesShiftDataResp == null
+				|| mesShiftDataResp.getMesShift() == null
+				|| StringUtils.isBlank(mesShiftDataResp.getStart())
+				|| mesShiftDataResp.getStart().length() < 10){
+			throw new ServiceException(text("当前时间未匹配到有效班次,无法匹配生产计划"));
+		}
+
+		String planDay = mesShiftDataResp.getStart().substring(0, 10);
+		MesProductPlan query = new MesProductPlan();
+		query.setShiftId(mesShiftDataResp.getMesShift().getId());
+		query.setDay(java.sql.Date.valueOf(planDay));
+		query.setBomProductCateId(cateSn);
+		query.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+		query.getSqlMap().getWhere().and("c.state", QueryType.EQ, "1");
+
+		List<MesProductPlan> plans = findList(query);
+		if(plans.isEmpty()){
+			return null;
+		}
+		if(plans.size() > 1){
+			throw new ServiceException(text("当前班次、该产品存在多条有效生产计划,请先处理重复计划"));
+		}
+		return plans.get(0);
+	}
+
+	/**
+	 * 校验计划剩余可生产数量,防止超产。
+	 * 一件产品在创建时即占用一个计划数量,因此这里统计已绑定该计划的产品数。
+	 *
+	 * @param plan 已匹配到的生产计划
+	 */
+	public void validatePlanCapacity(MesProductPlan plan){
+		if(plan == null || StringUtils.isBlank(plan.getId()) || plan.getNums() == null){
+			return;
+		}
+		MesProduct query = new MesProduct();
+		query.setPlanId(plan.getId());
+		long produced = mesProductService.findCount(query);
+		if(produced >= plan.getNums()){
+			throw new ServiceException(text("生产计划已完成,不能继续生产"));
 		}
-		return mesProductPlan1;
 	}
 	
 }
-

+ 24 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java

@@ -98,6 +98,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	private MesProductService mesProductService;
 
 	@Autowired
+	private MesProductPlanService mesProductPlanService;
+
+	@Autowired
 	private MesProductQualityService mesProductQualityService;
 
 	@Autowired
@@ -259,6 +262,18 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 		return mesProductRecordDao.selectByOprno(mesProductRecord);
 	}
 
+	/**
+	 * 创建产品前绑定当前班次的生产计划,并校验计划剩余数量。
+	 */
+	private MesProductPlan resolvePlan(String cateSn) {
+		MesProductPlan mesProductPlan = mesProductPlanService.getCurrentPlan(cateSn);
+		if (mesProductPlan == null) {
+			throw new ServiceException(text("当前班次没有有效的生产计划,请先排产!"));
+		}
+		mesProductPlanService.validatePlanCapacity(mesProductPlan);
+		return mesProductPlan;
+	}
+
 
 	/**
 	 * 保存数据(插入或更新)
@@ -284,6 +299,8 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				throw new ServiceException(text("产品类型不正确!"));
 			}
 
+			MesProductPlan mesProductPlan = resolvePlan(cateSn);
+
 			mesProductRecord.setOprno("OP040");
 			String oprno = CommonUitl.formatOprno(mesProductRecord.getOprno());
 
@@ -328,6 +345,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			mesProduct.setSn(mesProductRecord.getSn());
 			mesProduct.setState("0");
 			mesProduct.setCate(cateSn);
+			mesProduct.setPlanId(mesProductPlan.getId());
 			mesProductService.save(mesProduct);
 		}
 	}
@@ -350,6 +368,8 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				throw new ServiceException(text("未设置生产模板!"));
 			}
 
+			MesProductPlan mesProductPlan = resolvePlan(cateSn);
+
 			Integer i = 1;
 			Long cdate = Long.valueOf(CommonUitl.Date2TimeStamp(new Date()));
 			List<MesProductRecord> records = new ArrayList<>();
@@ -393,6 +413,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			mesProduct.setSn(mesProductRecord.getSn());
 			mesProduct.setState("0");
 			mesProduct.setCate(cateSn);
+			mesProduct.setPlanId(mesProductPlan.getId());
 			mesProductService.save(mesProduct);
 
 			if(source.equals("0")){ // 系统自动生成订单,扣减次数
@@ -467,6 +488,8 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				throw new ServiceException(text("未设置生产模板!"));
 			}
 
+			MesProductPlan mesProductPlan = resolvePlan(cateSn);
+
 			// 获取当前工位的设备及工装编号
 			MesLineProcess mesLineProcess = new MesLineProcess();
 			mesLineProcess.setOprno(mesProductRecord.getOprno());
@@ -538,6 +561,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			mesProduct.setSn(mesProductRecord.getSn());
 			mesProduct.setState("0");
 			mesProduct.setCate(cateSn);
+			mesProduct.setPlanId(mesProductPlan.getId());
 			mesProductService.save(mesProduct);
 
 			return "RSOK";

+ 20 - 2
src/main/java/com/jeesite/modules/mes/web/MesProductBbController.java

@@ -21,7 +21,9 @@ import org.springframework.web.bind.annotation.ResponseBody;
 import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
 import com.jeesite.common.web.BaseController;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.modules.mes.entity.MesProductBb;
+import com.jeesite.modules.mes.entity.MesShift;
 
 /**
  * 生产报表Controller
@@ -35,6 +37,8 @@ public class MesProductBbController extends BaseController {
 	@Autowired
 	private MesProductBbService mesProductBbService;
 	@Autowired
+	private MesShiftService mesShiftService;
+	@Autowired
 	private MesProductPlanService mesProductPlanService;
 	@Autowired
 	private MesProductRepairService mesProductRepairService;
@@ -58,10 +62,24 @@ public class MesProductBbController extends BaseController {
 	@RequestMapping(value = {"list", ""})
 	public String list(MesProductBb mesProductBb, Model model) {
 		model.addAttribute("mesProductBb", mesProductBb);
+		MesShift mesShift = new MesShift();
+		mesShift.setStatus("0");
+		model.addAttribute("shifts", mesShiftService.findList(mesShift));
 		return "modules/mes/mesProductBbList";
 	}
 
 	/**
+	 * 判断所选班次是否为跨天夜班。
+	 */
+	private boolean isNightShift(String shiftId) {
+		if (StringUtils.isBlank(shiftId)) {
+			return false;
+		}
+		MesShift mesShift = mesShiftService.get(new MesShift(shiftId));
+		return mesShift != null && Integer.valueOf(1).equals(mesShift.getNext());
+	}
+
+	/**
 	 * 查询列表数据
 	 */
 	@RequiresPermissions("mes:mesProductBb:view")
@@ -77,7 +95,7 @@ public class MesProductBbController extends BaseController {
 			calendar.setTime(startDate);
 			while (!calendar.getTime().after(endDate)) {
 				mesProductBb.setDay(calendar.getTime());
-				if ("2".equals(mesProductBb.getShiftId())) {
+				if (isNightShift(mesProductBb.getShiftId())) {
 					List<MesProductBb> list2 = mesProductBbService.findBbList2(mesProductBb);
 					if (list2 != null) resultList.addAll(list2);
 				} else {
@@ -89,7 +107,7 @@ public class MesProductBbController extends BaseController {
 			}
 		} else {
 			// 如果用户没有选择日期,走原来的单次查询逻辑兜底
-			if ("2".equals(mesProductBb.getShiftId())) {
+			if (isNightShift(mesProductBb.getShiftId())) {
 				List<MesProductBb> list2 = mesProductBbService.findBbList2(mesProductBb);
 				if (list2 != null) resultList.addAll(list2);
 			} else {

+ 20 - 2
src/main/java/com/jeesite/modules/mes/web/MesProductPlanController.java

@@ -4,6 +4,7 @@ import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.modules.mes.entity.MesProduct;
 import com.jeesite.modules.mes.entity.MesProductOrder;
 import com.jeesite.modules.mes.entity.MesShift;
@@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
 
 import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.ServiceException;
 import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.entity.MesProductPlan;
 import com.jeesite.modules.mes.service.MesProductPlanService;
@@ -60,6 +62,9 @@ public class MesProductPlanController extends BaseController {
 	@RequestMapping(value = {"list", ""})
 	public String list(MesProductPlan mesProductPlan, Model model) {
 		model.addAttribute("mesProductPlan", mesProductPlan);
+		MesShift mesShift = new MesShift();
+		mesShift.setStatus("0");
+		model.addAttribute("shifts", mesShiftService.findList(mesShift));
 		return "modules/mes/mesProductPlanList";
 	}
 	
@@ -70,6 +75,7 @@ public class MesProductPlanController extends BaseController {
 	@RequestMapping(value = "listData")
 	@ResponseBody
 	public Page<MesProductPlan> listData(MesProductPlan mesProductPlan, HttpServletRequest request, HttpServletResponse response) {
+		mesProductPlan.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
 		mesProductPlan.setPage(new Page<>(request, response));
 		Page<MesProductPlan> page = mesProductPlanService.findPage(mesProductPlan);
 		for (MesProductPlan mesProductPlan1 : page.getList()) {
@@ -83,6 +89,10 @@ public class MesProductPlanController extends BaseController {
 			if(!ObjectUtils.isEmpty(mesShift)){
 				mesProductPlan1.setShiftTitle(mesShift.getTitle());
 			}
+
+			MesProduct productQuery = new MesProduct();
+			productQuery.setPlanId(mesProductPlan1.getId());
+			mesProductPlan1.setNums2((int) mesProductService.findCount(productQuery));
 		}
 		return page;
 	}
@@ -122,7 +132,11 @@ public class MesProductPlanController extends BaseController {
 	@PostMapping(value = "save")
 	@ResponseBody
 	public String save(@Validated MesProductPlan mesProductPlan) {
-		mesProductPlanService.save(mesProductPlan);
+		try {
+			mesProductPlanService.save(mesProductPlan);
+		} catch (ServiceException e) {
+			return renderResult(Global.FALSE, e.getMessage());
+		}
 		return renderResult(Global.TRUE, text("保存生产计划成功!"));
 	}
 	
@@ -133,7 +147,11 @@ public class MesProductPlanController extends BaseController {
 	@RequestMapping(value = "delete")
 	@ResponseBody
 	public String delete(MesProductPlan mesProductPlan) {
-		mesProductPlanService.delete(mesProductPlan);
+		try {
+			mesProductPlanService.delete(mesProductPlan);
+		} catch (ServiceException e) {
+			return renderResult(Global.FALSE, e.getMessage());
+		}
 		return renderResult(Global.TRUE, text("删除生产计划成功!"));
 	}
 

+ 38 - 4
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -2930,6 +2930,12 @@ public class MesProductRecordController extends BaseController {
 			return resp;
 		}
 
+		String planError = checkCurrentPlan(cateSn);
+		if(planError != null){
+			resp.setMessage(planError);
+			return resp;
+		}
+
 		// 生成镭雕码
 		String sn = mesProductPreService.getNewSn(lineSn,"1",pn);
 		if(sn.isEmpty()){
@@ -2973,10 +2979,26 @@ public class MesProductRecordController extends BaseController {
 		return resp;
 	}
 
+	/**
+	 * 创建产品前预校验生产计划,避免在事务内抛异常导致接口返回 500。
+	 * 返回 null 表示校验通过,否则返回错误提示。
+	 */
+	private String checkCurrentPlan(String cateSn) {
+		try {
+			MesProductPlan mesProductPlan = mesProductPlanService.getCurrentPlan(cateSn);
+			if (mesProductPlan == null) {
+				return text("当前班次没有有效的生产计划,请先排产!");
+			}
+			mesProductPlanService.validatePlanCapacity(mesProductPlan);
+			return null;
+		} catch (RuntimeException e) {
+			return StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : text("生产计划校验失败,请联系管理员");
+		}
+	}
+
 	// 客户端创建订单接口
 	@PostMapping(value = "pccreateold")
 	@ResponseBody
-	@Transactional
 	public String pccreateold(HttpServletRequest request) {
 		mesLogService.apisavexz(request);
 		String sn = request.getParameter("sn");
@@ -3055,6 +3077,11 @@ public class MesProductRecordController extends BaseController {
 			return renderResult(Global.FALSE, text("产线不存在"));
 		}
 
+		String planError = checkCurrentPlan(cateSn);
+		if(planError != null){
+			return renderResult(Global.FALSE, planError);
+		}
+
 		MesProductRecord mesProductRecord = new MesProductRecord();
 		mesProductRecord.setSn(sn);
 		mesProductRecord.setOprno(oprno);
@@ -4968,9 +4995,16 @@ public class MesProductRecordController extends BaseController {
 			return renderResult(Global.FALSE, text("该产品已绑定包装码!"));
 		}
 
-		MesProductPlan mesProductPlan = mesProductPlanService.getCurPlan();
-		if(!ObjectUtils.isEmpty(mesProductPlan)){
-			mesProduct1.setPlanId(mesProductPlan.getId());
+		// 新件在创建时已绑定计划,这里只为历史未绑定件兜底,绝不覆盖已有计划。
+		if(StringUtils.isEmpty(mesProduct1.getPlanId())){
+			try {
+				MesProductPlan mesProductPlan = mesProductPlanService.getCurrentPlan(mesProduct1.getCate());
+				if(mesProductPlan != null){
+					mesProduct1.setPlanId(mesProductPlan.getId());
+				}
+			} catch (RuntimeException e) {
+				logger.warn("包装时匹配生产计划失败,保留原计划为空: {}", e.getMessage());
+			}
 		}
 
 		mesProduct1.setBatchSn(batchSn);

+ 1 - 0
src/main/java/com/jeesite/modules/wms/entity/WmsPick.java

@@ -25,6 +25,7 @@ import java.util.List;
 		@Column(name="id", attrName="id", label="id", isPK=true),
 		@Column(name="sn", attrName="sn", label="领料单号", queryType=QueryType.LIKE),
 		@Column(name="order_id", attrName="orderId", label="生产订单"),
+		@Column(name="plan_id", attrName="planId", label="生产计划"),
 		@Column(name="state", attrName="state", label="状态"),
 		@Column(name="remark", attrName="remark", label="备注"),
 		@Column(name="cate", attrName="cate", label="类型"),

+ 1 - 0
src/main/java/com/jeesite/modules/wms/web/WmsPickController.java

@@ -176,6 +176,7 @@ public class WmsPickController extends BaseController {
 
 		WmsPick wmsPick = new WmsPick();
 		wmsPick.setPlanId(planId);
+		wmsPick.setOrderId(mesProductOrder.getId());
 		wmsPick.setState("0");
 		wmsPickService.save(wmsPick);
 

+ 11 - 3
src/main/resources/mappings/modules/mes/MesProductBbDao.xml

@@ -15,7 +15,8 @@
     <select id="findBbList" resultType="com.jeesite.modules.mes.entity.MesProductBb">
         SELECT
         p.day AS "day",
-        p.shift_id AS "shift_id",
+        p.shift_id AS "shiftId",
+        ms.title AS "shiftTitle",
         p.nums AS "nums",
 
         (SELECT COUNT(1)
@@ -35,7 +36,9 @@
         AND CONCAT(DATE_FORMAT(p.day, '%Y-%m-%d'), ' 20:00:00')) AS "line_off"
 
         FROM mes_product_plan p
+        LEFT JOIN mes_shift ms ON ms.id = p.shift_id
         <where>
+            AND p.status = '0'
             <if test="day != null">
                 AND p.day = #{day}
             </if>
@@ -47,7 +50,9 @@
     <select id="findBbList2" resultType="com.jeesite.modules.mes.entity.MesProductBb">
         SELECT
         p.day AS "day",
-        p.shift_id AS "shift_id", p.nums AS "nums",
+        p.shift_id AS "shiftId",
+        ms.title AS "shiftTitle",
+        p.nums AS "nums",
 
         (SELECT COUNT(1)
         FROM mes_product_repair r
@@ -63,8 +68,11 @@
         FROM mes_product_batch b
         WHERE b.cate = '2'
         AND b.create_date BETWEEN CONCAT(DATE_FORMAT(p.day, '%Y-%m-%d'), ' 20:00:00')
-        AND CONCAT(DATE_FORMAT(DATE_ADD(p.day, INTERVAL 1 DAY), '%Y-%m-%d'), ' 08:00:00')) AS "line_off" FROM mes_product_plan p
+        AND CONCAT(DATE_FORMAT(DATE_ADD(p.day, INTERVAL 1 DAY), '%Y-%m-%d'), ' 08:00:00')) AS "line_off"
+        FROM mes_product_plan p
+        LEFT JOIN mes_shift ms ON ms.id = p.shift_id
         <where>
+            AND p.status = '0'
             <if test="day != null">
                 AND p.day = #{day}
             </if>

+ 2 - 13
src/main/resources/views/modules/mes/mesProductBbList.html

@@ -28,11 +28,7 @@
 			<div class="form-group">
 				<label class="control-label">${text('班次')}:</label>
 				<div class="control-inline">
-					<select name="shiftId" id="shiftId" class="form-control width-120">
-<!--						<option value="">${text('全部')}</option>-->
-						<option value="1" ${mesProductBb.shiftId == '1' ? 'selected' : ''}>${text('白班')}</option>
-						<option value="2" ${mesProductBb.shiftId == '2' ? 'selected' : ''}>${text('夜班')}</option>
-					</select>
+					<#form:select path="shiftId" items="${shifts}" itemLabel="title" itemValue="id" blankOption="true" class="form-control width-120" />
 				</div>
 			</div>
 
@@ -60,14 +56,7 @@
 					return val || '';
 				}},
 
-			{header:'${text("班次")}', name:'shiftId', index:'a.shift_id', width:150, align:"center", formatter: function(val, obj, row, act){
-					if (val == '1') {
-						return '<span class="text-primary">${text("白班")}</span>'; // 可以加点颜色,看起来更直观
-					} else if (val == '2') {
-						return '<span class="text-warning">${text("夜班")}</span>';
-					}
-					return val;
-				}},
+			{header:'${text("班次")}', name:'shiftTitle', index:'a.shift_id', width:150, align:"center"},
 
 			{header:'${text("计划数量")}', name:'nums', index:'a.nums', width:150, align:"center"},
 			{header:'${text("下线数量")}', name:'lineOff', index:'a.line_off', width:150, align:"center"},

+ 1 - 1
src/main/resources/views/modules/mes/mesProductOrderList.html

@@ -73,7 +73,7 @@ $('#dataGrid').dataGrid({
 					actions.push('<a href="${ctx}/mes/mesProductOrder/send?id='+row.id+'" class="btn btn-default btn-xs btnList" title="${text("下发")}" data-confirm="${text("确认下发该生产订单吗?")}"><i class="fa fa-check-square-o"></i> ${text("下发")}</a>&nbsp;');
 					actions.push('</div>');
 				}else if(row.state == "1"){
-					// actions.push('<a href="${ctx}/mes/mesProductOrder/plan?id='+row.id+'" class="btnList" title="${text("计划")}">计划</a>&nbsp;');
+					actions.push('<a href="${ctx}/mes/mesProductPlan/list?orderId='+row.id+'" class="btnList" title="${text("生产计划")}">计划</a>&nbsp;');
 				}
 				// actions.push('<a href="${ctx}/mes/mesProductOrder/pickDetail?id='+row.id+'" class="btnList" title="${text("查看订单详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
 								//<% } %>

+ 1 - 1
src/main/resources/views/modules/mes/mesProductPlanAdd.html

@@ -45,7 +45,7 @@
 							<label class="control-label col-sm-2" title="">
 								<span class="required hide">*</span> ${text('班次')}:<i class="fa icon-question hide"></i></label>
 							<div class="col-sm-4">
-								<#form:input path="shiftId" maxlength="64" class="form-control"/>
+								<#form:select path="shiftId" items="${shifts}" itemLabel="title" itemValue="id" blankOption="true" class="form-control required" />
 							</div>
 						</div>
 					</div>

+ 1 - 5
src/main/resources/views/modules/mes/mesProductPlanForm.html

@@ -45,11 +45,7 @@
 							<label class="control-label col-sm-2" title="">
 								<span class="required hide">*</span> ${text('班次')}:<i class="fa icon-question hide"></i></label>
 							<div class="col-sm-4">
-								<select name="shiftId" id="shiftId" class="form-control required">
-									<option value="">请选择</option>
-									<option value="1" ${mesProductPlan.shiftId == '1' ? 'selected' : ''}>白班</option>
-									<option value="2" ${mesProductPlan.shiftId == '2' ? 'selected' : ''}>夜班</option>
-								</select>
+								<#form:select path="shiftId" items="${shifts}" itemLabel="title" itemValue="id" blankOption="true" class="form-control required" />
 							</div>
 						</div>
 					</div>

+ 2 - 17
src/main/resources/views/modules/mes/mesProductPlanList.html

@@ -32,11 +32,7 @@
 			<div class="form-group">
 				<label class="control-label">${text('班次')}:</label>
 				<div class="control-inline width-120">
-					<select name="shiftId" class="form-control">
-						<option value="">全部</option>
-						<option value="1" ${mesProductPlan.shiftId == '1' ? 'selected' : ''}>白班</option>
-						<option value="2" ${mesProductPlan.shiftId == '2' ? 'selected' : ''}>夜班</option>
-					</select>
+					<#form:select path="shiftId" items="${shifts}" itemLabel="title" itemValue="id" blankOption="true" class="form-control" />
 				</div>
 			</div>
 				<div class="form-group">
@@ -58,18 +54,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("生产订单")}', name:'orderSn', index:'a.order_id', width:150, align:"center", frozen:true},
 		{header:'${text("日期")}', name:'day', index:'a.day', width:150, align:"center"},
 		{header:'${text("BOM")}', name:'bomTitle', index:'d.title', width:150, align:"center"},
-		{header:'${text("班次")}', name:'shiftTitle', index:'a.shift_id', width:150, align:"center", formatter: function(val, obj, row, act){
-				// 根据返回的班次值进行判断,兼容 val (shiftTitle) 或者是 row.shiftId
-				var shiftValue = val;
-				// 如果后端直接返回的是 1 或 2,在此进行拦截翻译
-				if (shiftValue == '1' || row.shiftId == '1') {
-					return '<span class="label label-success">白班</span>'; // 加点小样式更直观,不需要样式直接 return '白班' 即可
-				} else if (shiftValue == '2' || row.shiftId == '2') {
-					return '<span class="label label-primary">夜班</span>';
-				}
-				// 如果没有匹配上,则原样输出或显示空
-				return val || '';
-			}},
+		{header:'${text("班次")}', name:'shiftTitle', index:'a.shift_id', width:150, align:"center"},
 		{header:'${text("计划数量")}', name:'nums', index:'a.nums', width:150, align:"center"},
 		{header:'${text("生产数量")}', name:'nums2', index:'a.nums', width:150, align:"center"},
 		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},