Sfoglia il codice sorgente

feat: 下单选择产品类型,新增当前计划/当前产品接口

- 生产订单、生产计划新增 product_cate_id(产品类型/工件类型),打通 订单->计划->产品 链路
- 下单表单新增「产品类型」下拉:选BOM自动带出工件类型,新建订单按当前计划的产品类型默认带出
- 下单保存时校验产品类型:未选按BOM补齐、与BOM工件类型不一致则报错
- 排产时产品类型优先取订单,订单未维护时用BOM兜底;计划显式记录产品类型
- getCurrentPlan 匹配条件改为 plan.product_cate_id,不再依赖BOM是否维护工件类型
- 新增接口 POST /mes/mesProductPlan/currentPlan:获取当前班次计划与当前产品

需先执行 db/mes_product_cate_link.sql
hbx 5 giorni fa
parent
commit
234beeb03b

+ 9 - 1
api.http

@@ -454,4 +454,12 @@ token=1&pageNo=1&pageSize=20&__ajax=json&__sid=40ca1cc4f8a54d3a8c0cd327d30b4998&
 
 ###
 
-###__ajax=json&oprno=OP120A&lineSn=XT&sn=12345678&workNum=system&cxm=3&result=&testPressure=KyAxNzguMDA%3D&testPressureUnit=kPa&leakVal=KyAgIDAuMDA%3D&leakValUnit=Pa&testTime=MjAyNS0wOS0yNSAxMToyNzoyOA%3D%3D&title=5Y2K5oiQ5ZOB&remark=Stop&deviceType=innomatec&cq=&by=&cs=&leakRate=&leakRateUnit=&type=
+###__ajax=json&oprno=OP120A&lineSn=XT&sn=12345678&workNum=system&cxm=3&result=&testPressure=KyAxNzguMDA%3D&testPressureUnit=kPa&leakVal=KyAgIDAuMDA%3D&leakValUnit=Pa&testTime=MjAyNS0wOS0yNSAxMToyNzoyOA%3D%3D&title=5Y2K5oiQ5ZOB&remark=Stop&deviceType=innomatec&cq=&by=&cs=&leakRate=&leakRateUnit=&type=
+###
+
+# 获取当前班次的生产计划与当前产品(工件类型)
+# 返回:当前班次信息 + 当前产品(cateSn/cateTitle) + 当前计划明细(计划数量/已生产数量/订单号)
+POST http://127.0.0.1:8980/js/a/mes/mesProductPlan/currentPlan
+Content-Type: application/x-www-form-urlencoded
+
+__ajax=json&__sid=f9cea112ef6242cf820d950f870fb520

+ 79 - 0
db/mes_product_cate_link.sql

@@ -0,0 +1,79 @@
+-- 订单-计划-产品 全链路:增加"产品类型(工件类型)"字段
+-- 背景:
+--   1) 生产订单没有产品类型字段,产品类型只能从 BOM 的 product_cate_id 推导,
+--      BOM 未维护工件类型时订单无法排产、计划无法匹配产品;
+--   2) 生产计划没有产品类型字段,只能通过 计划->订单->BOM 三级关联反查,
+--      既慢又容易因 BOM 变更导致历史计划的产品类型错乱;
+--   3) 需要"当前计划/当前产品"接口,客户端/下单页面据此判断当前在生产哪种产品。
+-- 本脚本负责:
+--   1) mes_product_order 增加 product_cate_id(产品类型,存 mes_product_cate.sn)
+--   2) mes_product_plan  增加 product_cate_id(产品类型,存 mes_product_cate.sn)
+--   3) 存量数据回填
+-- 说明:字段必须先执行本脚本再发布应用。
+
+-- ---------- 1. 生产订单增加产品类型 ----------
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_order' AND COLUMN_NAME = 'product_cate_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_order` ADD COLUMN `product_cate_id` VARCHAR(50) DEFAULT NULL COMMENT ''产品类型(工件类型编号)'' AFTER `bom_id`',
+  'SELECT ''mes_product_order.product_cate_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 2. 生产计划增加产品类型 ----------
+SET @col_exists = (SELECT COUNT(1) FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_plan' AND COLUMN_NAME = 'product_cate_id');
+SET @ddl = IF(@col_exists = 0,
+  'ALTER TABLE `mes_product_plan` ADD COLUMN `product_cate_id` VARCHAR(50) DEFAULT NULL COMMENT ''产品类型(工件类型编号)'' AFTER `order_id`',
+  'SELECT ''mes_product_plan.product_cate_id 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 3. 存量回填:订单按 BOM 的工件类型补齐 ----------
+UPDATE `mes_product_order` o
+  JOIN `wms_bom` b ON b.`id` = o.`bom_id`
+  SET o.`product_cate_id` = b.`product_cate_id`
+ WHERE (o.`product_cate_id` IS NULL OR o.`product_cate_id` = '')
+   AND b.`product_cate_id` IS NOT NULL AND b.`product_cate_id` <> '';
+
+-- ---------- 4. 存量回填:计划按订单的产品类型补齐(订单为空时用 BOM 兜底) ----------
+UPDATE `mes_product_plan` p
+  JOIN `mes_product_order` o ON o.`id` = p.`order_id`
+  SET p.`product_cate_id` = IFNULL(NULLIF(o.`product_cate_id`, ''),
+        (SELECT b.`product_cate_id` FROM `wms_bom` b WHERE b.`id` = o.`bom_id`))
+ WHERE p.`product_cate_id` IS NULL OR p.`product_cate_id` = '';
+
+-- ---------- 5. 索引 ----------
+SET @idx_exists = (SELECT COUNT(1) FROM information_schema.STATISTICS
+  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mes_product_order' AND INDEX_NAME = 'idx_mpo_product_cate');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpo_product_cate` ON `mes_product_order` (`product_cate_id`)',
+  'SELECT ''idx_mpo_product_cate 已存在'' 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_plan' AND INDEX_NAME = 'idx_mpp_product_cate');
+SET @ddl = IF(@idx_exists = 0,
+  'CREATE INDEX `idx_mpp_product_cate` ON `mes_product_plan` (`product_cate_id`)',
+  'SELECT ''idx_mpp_product_cate 已存在'' AS message');
+PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- ---------- 6. 验证 ----------
+-- 6.1 字段
+SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('mes_product_order','mes_product_plan')
+   AND COLUMN_NAME = 'product_cate_id';
+-- 6.2 订单未回填(这些订单需在下单时补选产品类型)
+SELECT id, sn, bom_id FROM mes_product_order
+ WHERE product_cate_id IS NULL OR product_cate_id = '';
+-- 6.3 计划未回填(同上,排产时按订单自动补齐)
+SELECT p.id, p.order_id, p.day, p.nums FROM mes_product_plan p
+ WHERE p.product_cate_id IS NULL OR p.product_cate_id = '';
+-- 6.4 计划与订单产品类型不一致(应为 0 行)
+SELECT p.id, p.product_cate_id AS plan_cate, o.product_cate_id AS order_cate
+  FROM mes_product_plan p JOIN mes_product_order o ON o.id = p.order_id
+ WHERE IFNULL(p.product_cate_id,'') <> IFNULL(o.product_cate_id,'');
+-- 6.5 计划与产品类型主数据不一致(应为 0 行)
+SELECT DISTINCT p.product_cate_id FROM mes_product_plan p
+ WHERE p.product_cate_id IS NOT NULL AND p.product_cate_id <> ''
+   AND NOT EXISTS (SELECT 1 FROM mes_product_cate c WHERE c.sn = p.product_cate_id);
+
+SELECT '订单-计划 产品类型脚本执行完成' AS message;

+ 102 - 0
docs/生产计划-无计划拦截-实施方案.md

@@ -311,3 +311,105 @@ SELECT id, order_id, day, shift_id, nums FROM mes_product_plan
 - **关联字段查询**:JeeSite 会把 join 上的字段自动绑定为查询条件(`orderSn` 声明为 LIKE、`planDay` 声明为 EQ),控制器里另加了兜底条件,值一致不会冲突。
 - **拦截范围**:返修(`isRepair=1`)放行;开关关闭时全部放行;提交时**不再**校验超产数量(数量在创建工件时已扣)。
 
+---
+
+## 十、下单选择产品类型 + 当前计划/当前产品接口(已落地)
+
+### 10.1 目标
+
+把 **订单 → 计划 → 产品** 三段链路显式化:
+
+```
+生产订单.product_cate_id  →  生产计划.product_cate_id  →  工件(mes_product.cate)
+        (下单时选择)              (排产时继承订单)
+```
+
+- 下单时可直接选择产品类型(工件类型),不再强依赖 BOM 是否维护了工件类型;
+- 选择 BOM 时自动带出其工件类型;新建订单时按**当前计划的产品类型**默认带出;
+- 提供接口获取当前班次的计划与当前产品,客户端/下单页面据此判断"现在在生产什么"。
+
+### 10.2 数据模型(脚本 `db/mes_product_cate_link.sql`,已执行)
+
+| 表 | 新增字段 | 说明 |
+|---|---|---|
+| `mes_product_order` | `product_cate_id` | 产品类型(存 `mes_product_cate.sn`) |
+| `mes_product_plan` | `product_cate_id` | 产品类型(存 `mes_product_cate.sn`) |
+
+存量回填:订单按 BOM 的工件类型补齐;计划按订单的产品类型补齐(订单为空时用 BOM 兜底)。
+
+> 约定:全系统"工件类型"都用 `mes_product_cate.sn`(如 `CZ060`),与 `mes_line.product_cate`、
+> `wms_bom.product_cate_id` 保持一致(`wms_bom` 实体里按 id 关联的那处是历史遗留,不影响本链路)。
+
+### 10.3 下单(生产订单)
+
+| 位置 | 改动 |
+|---|---|
+| `MesProductOrderService.checkProductCate()` | 保存时校验:未选择产品类型 → 按 BOM 的工件类型自动补齐;所选类型必须存在且启用;与 BOM 的工件类型不一致 → 报错「所选产品类型与BOM的工件类型(CZ060)不一致,请核对后重试!」 |
+| `MesProductOrderController.form()` | 下发产品类型下拉(`mes_product_cate`,status=0)、BOM→工件类型映射、当前计划的产品类型 |
+| `mesProductOrderForm.html` | 新增「产品类型」下拉(必填);选择 BOM 自动带出工件类型;新建订单按当前计划的产品类型默认带出 |
+| `mesProductOrderList.html` | 新增「产品类型」「产品类型编号」列 |
+
+### 10.4 排产(生产计划)
+
+| 位置 | 改动 |
+|---|---|
+| `MesProductPlanService.save()` | 产品类型优先取订单选择的产品类型,订单未维护时用 BOM 兜底;写入 `plan.product_cate_id`;"同日期+班次+产品唯一"校验改用 `plan.product_cate_id`;编辑计划时产品类型跟随订单同步 |
+| `MesProductPlanService.getCurrentPlan(cateSn)` | 匹配条件由 BOM 关联改为 `plan.product_cate_id = cateSn`(不再依赖 BOM 是否维护工件类型) |
+| `mesProductPlanList.html` | 新增「产品类型」列 |
+
+### 10.5 接口:获取当前计划 / 当前产品
+
+```
+POST /mes/mesProductPlan/currentPlan
+```
+
+返回示例:
+
+```json
+{
+  "result": "true",
+  "message": "成功",
+  "data": {
+    "shiftId": "1700688350585237504",
+    "shiftTitle": "白班",
+    "shiftStart": "2026-10-21 08:00",
+    "shiftEnd": "2026-10-21 20:00",
+    "day": "2026-10-21",
+    "cateSn": "CZ060",
+    "cateTitle": "CZ060",
+    "planNums": 3,
+    "producedNums": 0,
+    "plans": [{
+      "planId": "2112728390803562496",
+      "orderId": "2112728164835434496",
+      "orderSn": "202610216998016",
+      "bomTitle": "初版AD02-BOM",
+      "cateSn": "CZ060",
+      "cateTitle": "CZ060",
+      "day": "2026-10-21",
+      "shiftTitle": "白班",
+      "nums": 3,
+      "produced": 0
+    }]
+  }
+}
+```
+
+- `cateSn`/`cateTitle`:**当前产品**(当前班次只有一个产品类型时给出;存在多个产品类型时为 null,用 `plans` 自行判断)
+- `plans`:**当前计划**明细(含订单号、计划数量、已生产数量)
+- 其它实现:`MesProductPlanService.getCurrentShiftPlans()` / `getCurrentProductCate()` / `getCurrentPlanData()`
+
+### 10.6 本地库执行验证(localhost:3307 / mes_cloud_xz)
+
+| 检查项 | 结果 |
+|---|---|
+| `mes_product_order.product_cate_id` | 已创建,4 个订单全部回填为 `CZ060` |
+| `mes_product_plan.product_cate_id` | 已创建,5/7 条计划回填为 `CZ060` |
+| 计划与订单产品类型不一致 | 0 行 ✓ |
+| 当前计划查询(白班 2026-10-21) | 返回 1 条:订单 `202610216998016`,CZ060,3 件 ✓ |
+| 订单列表 join 产品类型 | 正常 ✓ |
+
+> 剩余 2 条未回填的计划是历史脏数据(2026-04-16/17,关联订单已不存在、`shift_id='1'` 占位值),
+> 产品类型无从推导,且匹配不到真实班次,不影响排产与拦截,建议人工清理。
+
+

+ 25 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductOrder.java

@@ -23,6 +23,7 @@ import com.jeesite.modules.wms.entity.*;
 		@Column(name="nums", attrName="nums", label="生产数量", isUpdateForce=true),
 		@Column(name="delivery_date", attrName="deliveryDate", label="交货日期", isUpdateForce=true),
 		@Column(name="bom_id", attrName="bomId", label="BOM"),
+		@Column(name="product_cate_id", attrName="productCateId", label="产品类型"),
 		@Column(name="wms_order_id", attrName="wmsOrderId", label="销售单"),
 		@Column(name="remark", attrName="remark", label="备注"),
 		@Column(name="state", attrName="state", label="状态"),
@@ -42,6 +43,12 @@ import com.jeesite.modules.wms.entity.*;
 				columns={
 						@Column(name="order", attrName="wmsOrderSn", label="销售单号", queryType=QueryType.LIKE),
 				}),
+		// 产品类型(工件类型):存 mes_product_cate.sn
+		@JoinTable(type= JoinTable.Type.LEFT_JOIN, entity= MesProductCate.class, attrName="this", alias="e",
+				on="e.sn = a.product_cate_id",
+				columns={
+						@Column(name="title", attrName="productCateTitle", label="产品类型", queryType=QueryType.LIKE),
+				}),
 	}, orderBy="a.update_date DESC"
 )
 public class MesProductOrder extends DataEntity<MesProductOrder> {
@@ -51,6 +58,8 @@ public class MesProductOrder extends DataEntity<MesProductOrder> {
 	private Integer nums;		// 生产数量
 	private Date deliveryDate;		// 交货日期
 	private String bomId;		// BOM
+	private String productCateId;		// 产品类型(工件类型编号)
+	private String productCateTitle;		// 产品类型名称
 	private String remark;		// 备注
 	private String state;		// 状态
 	private String bomTitle;
@@ -101,6 +110,22 @@ public class MesProductOrder extends DataEntity<MesProductOrder> {
 	public void setBomId(String bomId) {
 		this.bomId = bomId;
 	}
+
+	public String getProductCateId() {
+		return productCateId;
+	}
+
+	public void setProductCateId(String productCateId) {
+		this.productCateId = productCateId;
+	}
+
+	public String getProductCateTitle() {
+		return productCateTitle;
+	}
+
+	public void setProductCateTitle(String productCateTitle) {
+		this.productCateTitle = productCateTitle;
+	}
 	
 	@Size(min=0, max=255, message="备注长度不能超过 255 个字符")
 	public String getRemark() {

+ 26 - 1
src/main/java/com/jeesite/modules/mes/entity/MesProductPlan.java

@@ -22,6 +22,7 @@ import com.jeesite.modules.wms.entity.WmsSupplier;
 @Table(name="mes_product_plan", alias="a", label="生产计划信息", columns={
 		@Column(name="id", attrName="id", label="id", isPK=true),
 		@Column(name="order_id", attrName="orderId", label="生产订单"),
+		@Column(name="product_cate_id", attrName="productCateId", label="产品类型"),
 		@Column(name="day", attrName="day", label="日期", isUpdateForce=true),
 		@Column(name="shift_id", attrName="shiftId", label="班次"),
 		@Column(name="is_pick", attrName="isPick", label="是否已生成领料单"),
@@ -46,7 +47,13 @@ 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),
+						@Column(name="product_cate_id", attrName="bomProductCateId", label="BOM工件类型", queryType=QueryType.EQ),
+				}),
+		// 产品类型名称(取自生产计划自身的 product_cate_id)
+		@JoinTable(type= JoinTable.Type.LEFT_JOIN, entity= MesProductCate.class, attrName="this", alias="e",
+				on="e.sn = a.product_cate_id",
+				columns={
+						@Column(name="title", attrName="productCateTitle", label="产品类型", queryType=QueryType.LIKE),
 				}),
 }, orderBy="a.update_date DESC"
 )
@@ -54,6 +61,8 @@ public class MesProductPlan extends DataEntity<MesProductPlan> {
 	
 	private static final long serialVersionUID = 1L;
 	private String orderId;		// 生产订单
+	private String productCateId;		// 产品类型(工件类型编号)
+	private String productCateTitle;		// 产品类型名称
 	@JsonFormat(pattern = "yyyy-MM-dd")
 	private Date day;		// 日期
 	private String shiftId;		// 班次
@@ -81,6 +90,22 @@ public class MesProductPlan extends DataEntity<MesProductPlan> {
 	public void setOrderId(String orderId) {
 		this.orderId = orderId;
 	}
+
+	public String getProductCateId() {
+		return productCateId;
+	}
+
+	public void setProductCateId(String productCateId) {
+		this.productCateId = productCateId;
+	}
+
+	public String getProductCateTitle() {
+		return productCateTitle;
+	}
+
+	public void setProductCateTitle(String productCateTitle) {
+		this.productCateTitle = productCateTitle;
+	}
 	
 	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 	public Date getDay() {

+ 111 - 0
src/main/java/com/jeesite/modules/mes/resp/MesCurrentPlanItemResp.java

@@ -0,0 +1,111 @@
+package com.jeesite.modules.mes.resp;
+
+/**
+ * 当前班次生产计划 明细
+ *
+ * @author hzd
+ * @version 2026-10-21
+ */
+public class MesCurrentPlanItemResp {
+
+	/** 生产计划id */
+	private String planId;
+	/** 生产订单id */
+	private String orderId;
+	/** 生产订单号 */
+	private String orderSn;
+	/** BOM名称 */
+	private String bomTitle;
+	/** 产品类型(工件类型编号) */
+	private String cateSn;
+	/** 产品类型名称 */
+	private String cateTitle;
+	/** 计划日期 */
+	private String day;
+	/** 班次名称 */
+	private String shiftTitle;
+	/** 计划数量 */
+	private Integer nums;
+	/** 已生产数量 */
+	private Integer produced;
+
+	public String getPlanId() {
+		return planId;
+	}
+
+	public void setPlanId(String planId) {
+		this.planId = planId;
+	}
+
+	public String getOrderId() {
+		return orderId;
+	}
+
+	public void setOrderId(String orderId) {
+		this.orderId = orderId;
+	}
+
+	public String getOrderSn() {
+		return orderSn;
+	}
+
+	public void setOrderSn(String orderSn) {
+		this.orderSn = orderSn;
+	}
+
+	public String getBomTitle() {
+		return bomTitle;
+	}
+
+	public void setBomTitle(String bomTitle) {
+		this.bomTitle = bomTitle;
+	}
+
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn;
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	public String getDay() {
+		return day;
+	}
+
+	public void setDay(String day) {
+		this.day = day;
+	}
+
+	public String getShiftTitle() {
+		return shiftTitle;
+	}
+
+	public void setShiftTitle(String shiftTitle) {
+		this.shiftTitle = shiftTitle;
+	}
+
+	public Integer getNums() {
+		return nums;
+	}
+
+	public void setNums(Integer nums) {
+		this.nums = nums;
+	}
+
+	public Integer getProduced() {
+		return produced;
+	}
+
+	public void setProduced(Integer produced) {
+		this.produced = produced;
+	}
+}

+ 115 - 0
src/main/java/com/jeesite/modules/mes/resp/MesCurrentPlanResp.java

@@ -0,0 +1,115 @@
+package com.jeesite.modules.mes.resp;
+
+import java.util.List;
+
+/**
+ * 当前班次生产计划/当前产品 返回数据
+ *
+ * 用于:客户端查询当前在生产什么产品;下订单时根据当前计划判断产品类型。
+ *
+ * @author hzd
+ * @version 2026-10-21
+ */
+public class MesCurrentPlanResp {
+
+	/** 当前班次id */
+	private String shiftId;
+	/** 当前班次名称 */
+	private String shiftTitle;
+	/** 当前班次开始时间 */
+	private String shiftStart;
+	/** 当前班次结束时间 */
+	private String shiftEnd;
+	/** 计划日期(班次开始日期) */
+	private String day;
+	/** 当前产品(工件类型编号,mes_product_cate.sn) */
+	private String cateSn;
+	/** 当前产品名称 */
+	private String cateTitle;
+	/** 当前计划数量合计 */
+	private Integer planNums;
+	/** 已生产数量合计 */
+	private Integer producedNums;
+	/** 当前班次计划明细 */
+	private List<MesCurrentPlanItemResp> plans;
+
+	public String getShiftId() {
+		return shiftId;
+	}
+
+	public void setShiftId(String shiftId) {
+		this.shiftId = shiftId;
+	}
+
+	public String getShiftTitle() {
+		return shiftTitle;
+	}
+
+	public void setShiftTitle(String shiftTitle) {
+		this.shiftTitle = shiftTitle;
+	}
+
+	public String getShiftStart() {
+		return shiftStart;
+	}
+
+	public void setShiftStart(String shiftStart) {
+		this.shiftStart = shiftStart;
+	}
+
+	public String getShiftEnd() {
+		return shiftEnd;
+	}
+
+	public void setShiftEnd(String shiftEnd) {
+		this.shiftEnd = shiftEnd;
+	}
+
+	public String getDay() {
+		return day;
+	}
+
+	public void setDay(String day) {
+		this.day = day;
+	}
+
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn;
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	public Integer getPlanNums() {
+		return planNums;
+	}
+
+	public void setPlanNums(Integer planNums) {
+		this.planNums = planNums;
+	}
+
+	public Integer getProducedNums() {
+		return producedNums;
+	}
+
+	public void setProducedNums(Integer producedNums) {
+		this.producedNums = producedNums;
+	}
+
+	public List<MesCurrentPlanItemResp> getPlans() {
+		return plans;
+	}
+
+	public void setPlans(List<MesCurrentPlanItemResp> plans) {
+		this.plans = plans;
+	}
+}

+ 45 - 1
src/main/java/com/jeesite/modules/mes/service/MesProductOrderService.java

@@ -4,9 +4,13 @@ import java.util.List;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
 import com.jeesite.modules.utils.HelperUtil;
+import com.jeesite.modules.wms.entity.WmsBom;
 import com.jeesite.modules.wms.entity.WmsOrder;
 import com.jeesite.modules.wms.entity.WmsSupplierOrder;
+import com.jeesite.modules.wms.service.WmsBomService;
 import com.jeesite.modules.wms.service.WmsOrderService;
 import com.twelvemonkeys.lang.StringUtil;
 import org.springframework.stereotype.Service;
@@ -14,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import com.jeesite.common.entity.Page;
 import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.entity.MesProductCate;
 import com.jeesite.modules.mes.entity.MesProductOrder;
 import com.jeesite.modules.mes.dao.MesProductOrderDao;
 
@@ -26,9 +31,14 @@ import com.jeesite.modules.mes.dao.MesProductOrderDao;
 public class MesProductOrderService extends CrudService<MesProductOrderDao, MesProductOrder> {
 
 	private final WmsOrderService wmsOrderService;
+	private final WmsBomService wmsBomService;
+	private final MesProductCateService mesProductCateService;
 
-	public MesProductOrderService(WmsOrderService wmsOrderService) {
+	public MesProductOrderService(WmsOrderService wmsOrderService, WmsBomService wmsBomService,
+			MesProductCateService mesProductCateService) {
 		this.wmsOrderService = wmsOrderService;
+		this.wmsBomService = wmsBomService;
+		this.mesProductCateService = mesProductCateService;
 	}
 
 	/**
@@ -95,8 +105,42 @@ public class MesProductOrderService extends CrudService<MesProductOrderDao, MesP
 			}
 		}
 
+		// 产品类型(下单时选择):未选择时按 BOM 的工件类型补齐,与 BOM 不一致时报错
+		checkProductCate(mesProductOrder);
+
 		super.save(mesProductOrder);
 	}
+
+	/**
+	 * 校验并补齐订单的产品类型。
+	 *
+	 * 规则:
+	 *   1) 订单未选择产品类型时,按所选 BOM 的工件类型自动补齐;
+	 *   2) 订单选择的产品类型必须存在且启用;
+	 *   3) BOM 已维护工件类型时,订单产品类型必须与 BOM 一致,否则提示核对。
+	 * @param mesProductOrder 生产订单
+	 */
+	private void checkProductCate(MesProductOrder mesProductOrder) {
+		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(mesProductOrder.getProductCateId())) {
+			// 未选择时按 BOM 的工件类型补齐
+			mesProductOrder.setProductCateId(bomCateSn);
+			return;
+		}
+		MesProductCate mesProductCate = mesProductCateService.findBySn(mesProductOrder.getProductCateId());
+		if (mesProductCate == null) {
+			throw new ServiceException(text("所选产品类型不存在或已停用,请重新选择!"));
+		}
+		if (StringUtils.isNotBlank(bomCateSn) && !bomCateSn.equals(mesProductOrder.getProductCateId())) {
+			throw new ServiceException(text("所选产品类型与BOM的工件类型(" + bomCateSn + ")不一致,请核对后重试!"));
+		}
+	}
 	
 	/**
 	 * 更新状态

+ 181 - 11
src/main/java/com/jeesite/modules/mes/service/MesProductPlanService.java

@@ -1,5 +1,8 @@
 package com.jeesite.modules.mes.service;
 
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
 import com.jeesite.common.lang.StringUtils;
@@ -8,6 +11,8 @@ 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.MesCurrentPlanItemResp;
+import com.jeesite.modules.mes.resp.MesCurrentPlanResp;
 import com.jeesite.modules.mes.resp.MesShiftDataResp;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -101,16 +106,12 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 				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();
-				}
-			}
+			String bomCateSn = resolveProductCate(mesProductOrder);
 			if(StringUtils.isBlank(bomCateSn)){
-				throw new ServiceException(text("生产订单的BOM未维护工件类型,无法排产"));
+				throw new ServiceException(text("生产订单未维护产品类型(工件类型),无法排产"));
 			}
+			// 计划显式记录产品类型,保证 订单->计划->产品 链路一致
+			mesProductPlan.setProductCateId(bomCateSn);
 
 			MesProductPlan query = new MesProductPlan();
 			query.setOrderId(mesProductPlan.getOrderId());
@@ -125,7 +126,7 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 			MesProductPlan slotQuery = new MesProductPlan();
 			slotQuery.setDay(mesProductPlan.getDay());
 			slotQuery.setShiftId(mesProductPlan.getShiftId());
-			slotQuery.setBomProductCateId(bomCateSn);
+			slotQuery.setProductCateId(bomCateSn);
 			slotQuery.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
 			if(!findList(slotQuery).isEmpty()){
 				throw new ServiceException(text("同一日期、班次、产品已存在生产计划,不能重复排产"));
@@ -146,9 +147,38 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 			}
 
 			mesProductPlan.setIsPick("0");
+		}else{
+			// 编辑计划时,产品类型跟随生产订单同步
+			MesProductOrder mesProductOrder = mesProductOrderService.get(new MesProductOrder(mesProductPlan.getOrderId()));
+			String productCateSn = resolveProductCate(mesProductOrder);
+			if(StringUtils.isNotBlank(productCateSn)){
+				mesProductPlan.setProductCateId(productCateSn);
+			}
 		}
 		super.save(mesProductPlan);
 	}
+
+	/**
+	 * 解析生产订单的产品类型:优先取订单选择的产品类型,订单未维护时用 BOM 的工件类型兜底。
+	 *
+	 * @param mesProductOrder 生产订单
+	 * @return 产品类型编号(mes_product_cate.sn)
+	 */
+	private String resolveProductCate(MesProductOrder mesProductOrder){
+		if(mesProductOrder == null){
+			return null;
+		}
+		if(StringUtils.isNotBlank(mesProductOrder.getProductCateId())){
+			return mesProductOrder.getProductCateId();
+		}
+		if(StringUtils.isNotBlank(mesProductOrder.getBomId())){
+			WmsBom wmsBom = wmsBomService.get(new WmsBom(mesProductOrder.getBomId()));
+			if(wmsBom != null){
+				return wmsBom.getProductCateId();
+			}
+		}
+		return null;
+	}
 	
 	/**
 	 * 更新状态
@@ -182,7 +212,7 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 	 *
 	 * 计划日期以“班次开始日期”为准,因此跨零点的夜班在次日凌晨仍然匹配前一天的计划。
 	 *
-	 * @param cateSn 产品工件类型(对应 wms_bom.product_cate_id)
+	 * @param cateSn 产品工件类型(对应 mes_product_cate.sn / 计划product_cate_id)
 	 * @return 唯一计划;没有计划时返回 null
 	 */
 	public MesProductPlan getCurrentPlan(String cateSn){
@@ -202,7 +232,7 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 		MesProductPlan query = new MesProductPlan();
 		query.setShiftId(mesShiftDataResp.getMesShift().getId());
 		query.setDay(java.sql.Date.valueOf(planDay));
-		query.setBomProductCateId(cateSn);
+		query.setProductCateId(cateSn);
 		query.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
 		query.getSqlMap().getWhere().and("c.state", QueryType.EQ, "1");
 
@@ -300,5 +330,145 @@ public class MesProductPlanService extends CrudService<MesProductPlanDao, MesPro
 		product.setPlanId(planId);
 		mesProductService.save(product);
 	}
+
+	/**
+	 * 获取当前班次的全部有效生产计划(订单已下发、计划未删除)。
+	 *
+	 * 计划日期以“班次开始日期”为准,跨零点的夜班在次日凌晨仍然匹配前一天的计划。
+	 *
+	 * @return 当前班次的计划列表;没有匹配到班次时返回空列表
+	 */
+	public List<MesProductPlan> getCurrentShiftPlans(){
+		List<MesProductPlan> plans = new ArrayList<>();
+		MesShiftDataResp mesShiftDataResp = mesShiftService.getCurShift();
+		if(mesShiftDataResp == null
+				|| mesShiftDataResp.getMesShift() == null
+				|| StringUtils.isBlank(mesShiftDataResp.getStart())
+				|| mesShiftDataResp.getStart().length() < 10){
+			return plans;
+		}
+		String planDay = mesShiftDataResp.getStart().substring(0, 10);
+		MesProductPlan query = new MesProductPlan();
+		query.setShiftId(mesShiftDataResp.getMesShift().getId());
+		query.setDay(java.sql.Date.valueOf(planDay));
+		query.getSqlMap().getWhere().and("a.status", QueryType.EQ, "0");
+		query.getSqlMap().getWhere().and("c.state", QueryType.EQ, "1");
+		List<MesProductPlan> list = findList(query);
+		if(list != null){
+			plans.addAll(list);
+		}
+		return plans;
+	}
+
+	/**
+	 * 获取当前班次正在生产的产品(工件类型)。
+	 * 当前班次只有一个产品类型时返回该类型;存在多个时返回 null。
+	 *
+	 * @return 产品类型编号(mes_product_cate.sn)
+	 */
+	public String getCurrentProductCate(){
+		List<MesProductPlan> plans = getCurrentShiftPlans();
+		if(plans.isEmpty()){
+			return null;
+		}
+		String cateSn = null;
+		for(MesProductPlan plan : plans){
+			if(StringUtils.isBlank(plan.getProductCateId())){
+				continue;
+			}
+			if(cateSn == null){
+				cateSn = plan.getProductCateId();
+			}else if(!cateSn.equals(plan.getProductCateId())){
+				// 当前班次存在多个产品类型,无法唯一判断
+				return null;
+			}
+		}
+		return cateSn;
+	}
+
+	/**
+	 * 获取当前班次的生产计划与当前产品(工件类型),供客户端/下单页面使用。
+	 *
+	 * @return 当前计划数据
+	 */
+	public MesCurrentPlanResp getCurrentPlanData(){
+		MesCurrentPlanResp resp = new MesCurrentPlanResp();
+		MesShiftDataResp shift = mesShiftService.getCurShift();
+		if(shift != null && shift.getMesShift() != null){
+			resp.setShiftId(shift.getMesShift().getId());
+			resp.setShiftTitle(shift.getMesShift().getTitle());
+			resp.setShiftStart(shift.getStart());
+			resp.setShiftEnd(shift.getEnd());
+			if(StringUtils.isNotBlank(shift.getStart()) && shift.getStart().length() >= 10){
+				resp.setDay(shift.getStart().substring(0, 10));
+			}
+		}
+
+		List<MesCurrentPlanItemResp> items = new ArrayList<>();
+		int planNums = 0;
+		int producedNums = 0;
+		for(MesProductPlan plan : getCurrentShiftPlans()){
+			MesCurrentPlanItemResp item = new MesCurrentPlanItemResp();
+			item.setPlanId(plan.getId());
+			item.setOrderId(plan.getOrderId());
+			item.setOrderSn(plan.getOrderSn());
+			item.setBomTitle(plan.getBomTitle());
+			item.setCateSn(plan.getProductCateId());
+			item.setCateTitle(plan.getProductCateTitle());
+			item.setDay(formatPlanDay(plan.getDay()));
+			item.setShiftTitle(plan.getShiftTitle());
+			item.setNums(plan.getNums());
+			int produced = countProduced(plan.getId());
+			item.setProduced(produced);
+			if(plan.getNums() != null){
+				planNums += plan.getNums();
+			}
+			producedNums += produced;
+			items.add(item);
+		}
+		resp.setPlans(items);
+		resp.setPlanNums(planNums);
+		resp.setProducedNums(producedNums);
+
+		String cateSn = getCurrentProductCate();
+		resp.setCateSn(cateSn);
+		if(cateSn != null){
+			for(MesCurrentPlanItemResp item : items){
+				if(cateSn.equals(item.getCateSn())){
+					resp.setCateTitle(item.getCateTitle());
+					break;
+				}
+			}
+		}
+		return resp;
+	}
+
+	/**
+	 * 统计计划下已生产的件数。
+	 *
+	 * @param planId 生产计划id
+	 * @return 已生产件数
+	 */
+	private int countProduced(String planId){
+		if(StringUtils.isBlank(planId)){
+			return 0;
+		}
+		MesProduct query = new MesProduct();
+		query.setPlanId(planId);
+		return (int) mesProductService.findCount(query);
+	}
+
+	/**
+	 * 格式化计划日期。
+	 *
+	 * @param day 计划日期
+	 * @return yyyy-MM-dd
+	 */
+	private String formatPlanDay(Date day){
+		if(day == null){
+			return null;
+		}
+		return new SimpleDateFormat("yyyy-MM-dd").format(day);
+	}
 	
 }

+ 28 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductOrderController.java

@@ -9,6 +9,7 @@ import java.text.SimpleDateFormat;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONValidator;
 import com.jeesite.common.codec.EncodeUtils;
 import com.jeesite.modules.wms.entity.WmsBom;
@@ -38,6 +39,7 @@ import com.jeesite.common.config.Global;
 import com.jeesite.common.entity.Page;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
 import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProductCate;
 import com.jeesite.modules.mes.entity.MesProductOrder;
 import com.jeesite.modules.mes.entity.MesProductPlan;
 import com.jeesite.modules.mes.service.MesProductOrderService;
@@ -67,6 +69,8 @@ public class MesProductOrderController extends BaseController {
     @Autowired private com.jeesite.modules.wms.service.WmsPickMaterialService wmsPickMaterialService;
     @Autowired
     private com.jeesite.modules.mes.service.MesProductPlanService mesProductPlanService;
+    @Autowired
+    private com.jeesite.modules.mes.service.MesProductCateService mesProductCateService;
 
 	/**
 	 * 获取数据
@@ -125,6 +129,30 @@ public class MesProductOrderController extends BaseController {
 		List<WmsBom> boms = wmsBomService.findList(wmsBom);
 		model.addAttribute("boms", boms);
 
+		// 产品类型(工件类型)下拉
+		MesProductCate cateQuery = new MesProductCate();
+		cateQuery.setStatus("0");
+		List<MesProductCate> cates = mesProductCateService.findList(cateQuery);
+		model.addAttribute("cates", cates);
+
+		// BOM -> 工件类型 映射,选择BOM时自动带出产品类型
+		Map<String, String> bomCateMap = new HashMap<>();
+		for (WmsBom bom : boms) {
+			if (bom.getProductCateId() != null) {
+				bomCateMap.put(bom.getId(), bom.getProductCateId());
+			}
+		}
+		model.addAttribute("bomCateMap", JSON.toJSONString(bomCateMap));
+
+		// 当前计划的产品类型:下单时据此判断产品类型(新建时默认带出)
+		String curCateSn = null;
+		try {
+			curCateSn = mesProductPlanService.getCurrentProductCate();
+		} catch (Exception e) {
+			logger.warn("获取当前计划产品类型失败:{}", e.getMessage());
+		}
+		model.addAttribute("curCateSn", curCateSn);
+
 		return "modules/mes/mesProductOrderForm";
 	}
 

+ 24 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductPlanController.java

@@ -28,6 +28,7 @@ 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.resp.CommonResp;
 import com.jeesite.modules.mes.service.MesProductPlanService;
 
 /**
@@ -103,6 +104,29 @@ public class MesProductPlanController extends BaseController {
 	}
 
 	/**
+	 * 获取当前班次的生产计划与当前产品(工件类型)。
+	 *
+	 * 用途:客户端显示"当前在生产什么产品";下订单时根据当前计划判断产品类型。
+	 *
+	 * @return 当前班次计划、当前产品
+	 */
+	@PostMapping(value = "currentPlan")
+	@ResponseBody
+	public CommonResp currentPlan() {
+		CommonResp resp = new CommonResp();
+		try {
+			resp.setData(mesProductPlanService.getCurrentPlanData());
+			resp.setResult(Global.TRUE);
+			resp.setMessage(text("成功"));
+		} catch (Exception e) {
+			logger.error("获取当前生产计划失败", e);
+			resp.setResult(Global.FALSE);
+			resp.setMessage(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : text("获取当前生产计划失败!"));
+		}
+		return resp;
+	}
+
+	/**
 	 * 查看编辑表单
 	 */
 	@RequiresPermissions("mes:mesProductPlan:view")

+ 39 - 0
src/main/resources/views/modules/mes/mesProductOrderForm.html

@@ -65,6 +65,18 @@
 					<div class="col-xs-12">
 						<div class="form-group">
 							<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:select path="productCateId" items="${cates}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control required" />
+								<span class="help-block" id="cateTip"></span>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="row">
+					<div class="col-xs-12">
+						<div class="form-group">
+							<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="remark" maxlength="255" class="form-control"/>
@@ -111,4 +123,31 @@ $("#inputForm").validate({
 		}, "json");
     }
 });
+
+// BOM -> 工件类型 映射(选择BOM时自动带出产品类型)
+var bomCateMap = ${bomCateMap!'{}'};
+// 当前计划的产品类型(新建订单时默认带出)
+var curCateSn = '${curCateSn!}';
+
+$(function(){
+	// 新建订单且未选产品类型时,按当前计划的产品类型带出
+	if(!$('#productCateId').val() && curCateSn){
+		$('#productCateId').val(curCateSn);
+		showCateTip('已按当前计划的产品类型带出,可手动调整');
+	}
+	// 切换BOM时自动带出对应的工件类型
+	$('#bomId').on('change', function(){
+		var cate = bomCateMap[$('#bomId').val()];
+		if(cate){
+			$('#productCateId').val(cate);
+			showCateTip('已按BOM的工件类型自动带出');
+		}else{
+			showCateTip('该BOM未维护工件类型,请手动选择产品类型');
+		}
+	});
+});
+
+function showCateTip(msg){
+	$('#cateTip').text(msg || '');
+}
 </script>

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

@@ -58,6 +58,8 @@ $('#dataGrid').dataGrid({
 		{header:'${text("未排产")}', name:'unPlanNums', index:'a.nums', width:150, align:"center", sortable:false},
 		{header:'${text("交货日期")}', name:'deliveryDate', index:'a.delivery_date', width:150, align:"center"},
 		{header:'${text("BOM")}', name:'bomTitle', index:'b.title', width:150, align:"center"},
+		{header:'${text("产品类型")}', name:'productCateTitle', index:'e.title', width:120, align:"center", sortable:false},
+		{header:'${text("产品类型编号")}', name:'productCateId', index:'a.product_cate_id', width:120, align:"center", sortable:false},
 		{header:'${text("备注")}', name:'remark', index:'a.remark', width:150, align:"center"},
 		{header:'${text("状态")}', name:'state', index:'a.state', width:150, align:"center", formatter: function(val, obj, row, act){
 			return js.getDictLabel(${@DictUtils.getDictListJson('wms_order_state')}, val, '${text("未知")}', true);

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

@@ -58,6 +58,7 @@ $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
 		{header:'${text("生产订单")}', name:'orderSn', index:'a.order_id', width:150, align:"center", frozen:true},
+		{header:'${text("产品类型")}', name:'productCateTitle', index:'e.title', width:120, align:"center", sortable:false},
 		{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"},