Bläddra i källkod

新增大屏,新增大屏管理

hou 5 dagar sedan
förälder
incheckning
beceec32d5

+ 98 - 0
db/mysql/mes_work_order.sql

@@ -0,0 +1,98 @@
+-- 工单:维护和导入。不参与现场上线。
+CREATE TABLE IF NOT EXISTS `mes_work_order` (
+  `id` varchar(64) NOT NULL COMMENT '主键',
+  `work_no` varchar(64) NOT NULL COMMENT '工单号,M开头',
+  `project_code` varchar(64) DEFAULT NULL COMMENT '项目别',
+  `material_sn` varchar(64) NOT NULL COMMENT '总成料号,1开头',
+  `spec_name` varchar(255) DEFAULT NULL COMMENT '规格名称',
+  `drawing_no` varchar(128) DEFAULT NULL COMMENT '图号',
+  `plan_qty` int(11) NOT NULL COMMENT '需生产数量',
+  `open_date` date NOT NULL COMMENT '开单日期',
+  `produce_date` date NOT NULL COMMENT '生产日期',
+  `sale_order_no` varchar(64) DEFAULT NULL COMMENT '订单编号',
+  `line_sn` varchar(64) DEFAULT NULL COMMENT '产线编号,下达前填写',
+  `cate_sn` varchar(64) DEFAULT NULL COMMENT '工件类型编号',
+  `state` char(1) NOT NULL DEFAULT '0' COMMENT '0待下达 1已下达 2生产中 3完工 4暂停 5关闭',
+  `done_qty` int(11) NOT NULL DEFAULT 0 COMMENT '已完工数量,本阶段固定为0',
+  `status` char(1) NOT NULL DEFAULT '0' COMMENT '状态(0正常 1删除 2停用)',
+  `create_by` varchar(64) NOT NULL COMMENT '创建者',
+  `create_date` datetime NOT NULL COMMENT '创建时间',
+  `update_by` varchar(64) NOT NULL COMMENT '更新者',
+  `update_date` datetime NOT NULL COMMENT '更新时间',
+  `remarks` varchar(500) DEFAULT NULL COMMENT '备注信息',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_mes_work_order_no` (`work_no`),
+  KEY `idx_mes_work_order_produce` (`produce_date`),
+  KEY `idx_mes_work_order_state` (`state`),
+  KEY `idx_mes_work_order_material` (`material_sn`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工单';
+
+-- 菜单挂在「生产线管理」下。执行后重新登录刷新菜单。
+DELETE FROM `js_sys_role_menu` WHERE `menu_code` = '2099101000000000001';
+DELETE FROM `js_sys_menu` WHERE `menu_code` = '2099101000000000001';
+
+INSERT INTO `js_sys_menu`
+(`menu_code`, `parent_code`, `parent_codes`, `tree_sort`, `tree_sorts`, `tree_leaf`, `tree_level`,
+ `tree_names`, `menu_name`, `menu_type`, `menu_href`, `menu_target`, `menu_icon`, `menu_color`, `menu_title`,
+ `permission`, `weight`, `is_show`, `sys_code`, `module_codes`, `component`, `params`, `status`,
+ `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT
+'2099101000000000001', p.`menu_code`, CONCAT(p.`parent_codes`, p.`menu_code`, ','),
+180, CONCAT(p.`tree_sorts`, '0000000180,'), '1', p.`tree_level` + 1,
+CONCAT(p.`tree_names`, '/工单管理'), '工单管理', '1',
+'/mes/mesWorkOrder/list', '', 'fa fa-list-alt', '', '',
+'mes:mesWorkOrder:view,mes:mesWorkOrder:edit', 40, '1', 'default', 'core', NULL, NULL, '0',
+'system', NOW(), 'system', NOW(), '工单维护与导入,不影响现场生产'
+FROM `js_sys_menu` p
+WHERE p.`menu_code` = '1680784143702568960';
+
+INSERT INTO `js_sys_role_menu` (`role_code`, `menu_code`)
+SELECT DISTINCT rm.`role_code`, '2099101000000000001'
+FROM `js_sys_role_menu` rm
+INNER JOIN `js_sys_menu` m ON m.`menu_code` = rm.`menu_code`
+WHERE m.`parent_code` = '1680784143702568960'
+  AND NOT EXISTS (
+    SELECT 1 FROM `js_sys_role_menu` x
+    WHERE x.`role_code` = rm.`role_code`
+      AND x.`menu_code` = '2099101000000000001'
+  );
+
+-- 上线工单校验开关。1 生效,0 关闭。在参数设置中修改,修改后重启应用。
+INSERT INTO `js_sys_config` (`id`, `config_name`, `config_key`, `config_value`, `is_sys`, `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT '2099101000000000002', '生产工单-上线校验开关', 'mes.workOrder.check', '1', '0', 'system', NOW(), 'system', NOW(), '1:上线校验工单并回写完工数量;0:不校验、不回写'
+FROM DUAL
+WHERE NOT EXISTS (
+  SELECT 1 FROM `js_sys_config` WHERE `config_key` = 'mes.workOrder.check'
+);
+
+-- 工单工件页。执行后重新登录刷新菜单。
+INSERT INTO `js_sys_menu`
+(`menu_code`, `parent_code`, `parent_codes`, `tree_sort`, `tree_sorts`, `tree_leaf`, `tree_level`,
+ `tree_names`, `menu_name`, `menu_type`, `menu_href`, `menu_target`, `menu_icon`, `menu_color`, `menu_title`,
+ `permission`, `weight`, `is_show`, `sys_code`, `module_codes`, `component`, `params`, `status`,
+ `create_by`, `create_date`, `update_by`, `update_date`, `remarks`)
+SELECT
+'2099101000000000003', p.`menu_code`, CONCAT(p.`parent_codes`, p.`menu_code`, ','),
+190, CONCAT(p.`tree_sorts`, '0000000190,'), '1', p.`tree_level` + 1,
+CONCAT(p.`tree_names`, '/工单工件'), '工单工件', '1',
+'/mes/mesWorkOrder/snList', '', 'fa fa-barcode', '', '',
+'mes:mesWorkOrder:view', 40, '1', 'default', 'core', NULL, NULL, '0',
+'system', NOW(), 'system', NOW(), '查看工单下的工件及状态'
+FROM `js_sys_menu` p
+WHERE p.`menu_code` = '1680784143702568960'
+  AND NOT EXISTS (SELECT 1 FROM `js_sys_menu` WHERE `menu_code` = '2099101000000000003');
+
+INSERT INTO `js_sys_role_menu` (`role_code`, `menu_code`)
+SELECT DISTINCT rm.`role_code`, '2099101000000000003'
+FROM `js_sys_role_menu` rm
+WHERE rm.`menu_code` = '2099101000000000001'
+  AND NOT EXISTS (
+    SELECT 1 FROM `js_sys_role_menu` x
+    WHERE x.`role_code` = rm.`role_code`
+      AND x.`menu_code` = '2099101000000000003'
+  );
+
+-- 上线记录回写工单号。已有库执行一次即可。
+ALTER TABLE `mes_product_record`
+  ADD COLUMN `work_no` varchar(64) DEFAULT NULL COMMENT '工单号' AFTER `line_sn`,
+  ADD INDEX `idx_mes_product_record_work_no` (`work_no`);

+ 27 - 0
src/main/java/com/jeesite/modules/mes/dao/MesWorkOrderDao.java

@@ -0,0 +1,27 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesWorkOrder;
+import com.jeesite.modules.mes.entity.MesWorkOrderSn;
+
+import java.util.List;
+
+/**
+ * 工单DAO接口
+ * @author mes
+ * @version 2026-10-10
+ */
+@MyBatisDao
+public interface MesWorkOrderDao extends CrudDao<MesWorkOrder> {
+
+	MesWorkOrder findByWorkNo(String workNo);
+
+	List<MesWorkOrder> findOnline(String lineSn);
+
+	int countOnlineOk(String workNo);
+
+	int updateDone(MesWorkOrder mesWorkOrder);
+
+	List<MesWorkOrderSn> findSnList(MesWorkOrderSn query);
+}

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

@@ -30,6 +30,7 @@ import java.util.List;
 //		@Column(name="message_type", attrName="messageType", label="报文类型", queryType=QueryType.LIKE),
 		@Column(name="cate_sn", attrName="cateSn", label="工件类型", queryType=QueryType.LIKE),
 		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.LIKE),
+		@Column(name="work_no", attrName="workNo", label="工单号", queryType=QueryType.EQ),
 		@Column(name="source", attrName="source", label="来源", queryType=QueryType.EQ),
 		@Column(name="materiel_sn", attrName="materielSn", label="物料批次二维码", queryType=QueryType.LIKE),
 		@Column(name="content", attrName="content", label="结果", queryType=QueryType.LIKE),
@@ -78,6 +79,7 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 	private Long cdate;
 	private Date updateDate;
 	private String lineSn;
+	private String workNo;
 	private String zcontent;
 	private String ngType;
 	private String processState; // 加工状态
@@ -277,6 +279,14 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 		this.lineSn = lineSn;
 	}
 
+	public String getWorkNo() {
+		return workNo;
+	}
+
+	public void setWorkNo(String workNo) {
+		this.workNo = workNo;
+	}
+
 	public Date getUpdateDate_gte() {
 		return sqlMap.getWhere().getValue("update_date", QueryType.GTE);
 	}

+ 295 - 0
src/main/java/com/jeesite/modules/mes/entity/MesWorkOrder.java

@@ -0,0 +1,295 @@
+package com.jeesite.modules.mes.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.JoinTable;
+import com.jeesite.common.mybatis.annotation.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.util.Date;
+
+/**
+ * 工单
+ * @author mes
+ * @version 2026-10-10
+ */
+@Table(name="mes_work_order", alias="a", label="工单信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(name="work_no", attrName="workNo", label="工单号", queryType=QueryType.LIKE),
+		@Column(name="project_code", attrName="projectCode", label="项目别", queryType=QueryType.LIKE),
+		@Column(name="material_sn", attrName="materialSn", label="总成料号", queryType=QueryType.LIKE),
+		@Column(name="spec_name", attrName="specName", label="规格名称", queryType=QueryType.LIKE),
+		@Column(name="drawing_no", attrName="drawingNo", label="图号", queryType=QueryType.LIKE),
+		@Column(name="plan_qty", attrName="planQty", label="需生产数量"),
+		@Column(name="open_date", attrName="openDate", label="开单日期"),
+		@Column(name="produce_date", attrName="produceDate", label="生产日期"),
+		@Column(name="sale_order_no", attrName="saleOrderNo", label="订单编号", queryType=QueryType.LIKE),
+		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.EQ),
+		@Column(name="cate_sn", attrName="cateSn", label="工件类型", queryType=QueryType.EQ),
+		@Column(name="state", attrName="state", label="状态", queryType=QueryType.EQ),
+		@Column(name="done_qty", attrName="doneQty", label="已完工数量"),
+		@Column(name="status", attrName="status", label="状态", isUpdate=false),
+		@Column(name="create_by", attrName="createBy", label="创建者", isUpdate=false, isQuery=false),
+		@Column(name="create_date", attrName="createDate", label="创建时间", isUpdate=false, isQuery=false, isUpdateForce=true),
+		@Column(name="update_by", attrName="updateBy", label="更新者", isQuery=false),
+		@Column(name="update_date", attrName="updateDate", label="更新时间", isQuery=false, isUpdateForce=true),
+		@Column(name="remarks", attrName="remarks", label="备注"),
+	}, joinTable={
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLine.class, attrName="this", alias="b",
+				on="b.sn = a.line_sn and b.status = '0' and b.id = (select min(l.id) from mes_line l where l.sn = a.line_sn and l.status = '0')",
+				columns={
+						@Column(name="title", attrName="lineTitle", label="产线"),
+				}),
+		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesProductCate.class, attrName="this", alias="c",
+				on="c.sn = a.cate_sn and c.status = '0'",
+				columns={
+						@Column(name="title", attrName="cateTitle", label="工件类型"),
+				}),
+	}, orderBy="a.produce_date DESC, a.update_date DESC"
+)
+public class MesWorkOrder extends DataEntity<MesWorkOrder> {
+
+	private static final long serialVersionUID = 1L;
+
+	/** 待下达 */
+	public static final String STATE_DRAFT = "0";
+	/** 已下达 */
+	public static final String STATE_ISSUED = "1";
+	/** 生产中,本阶段不写入 */
+	public static final String STATE_PRODUCING = "2";
+	/** 完工,本阶段不写入 */
+	public static final String STATE_DONE = "3";
+	/** 暂停,本阶段不写入 */
+	public static final String STATE_PAUSE = "4";
+	/** 关闭 */
+	public static final String STATE_CLOSED = "5";
+
+	private String workNo;
+	private String projectCode;
+	private String materialSn;
+	private String specName;
+	private String drawingNo;
+	private Integer planQty;
+	private Date openDate;
+	private Date produceDate;
+	private String saleOrderNo;
+	private String lineSn;
+	private String lineTitle;
+	private String cateSn;
+	private String cateTitle;
+	private String state;
+	private Integer doneQty;
+	/** 样表第 1 列,只用于对齐导入列,不入库 */
+	private Integer rowIndex;
+
+	@ExcelFields({
+			@ExcelField(title="序号", attrName="rowIndex", align=Align.CENTER, sort=10),
+			@ExcelField(title="工单号", attrName="workNo", align=Align.CENTER, sort=20),
+			@ExcelField(title="项目别", attrName="projectCode", align=Align.CENTER, sort=30),
+			@ExcelField(title="总成料号", attrName="materialSn", align=Align.CENTER, sort=40),
+			@ExcelField(title="规格名称", attrName="specName", align=Align.CENTER, sort=50),
+			@ExcelField(title="图号", attrName="drawingNo", align=Align.CENTER, sort=60),
+			@ExcelField(title="需生产数量", attrName="planQty", align=Align.CENTER, sort=70),
+			@ExcelField(title="开单日期", attrName="openDate", align=Align.CENTER, sort=80, dataFormat="yyyy-MM-dd"),
+			@ExcelField(title="生产日期", attrName="produceDate", align=Align.CENTER, sort=90, dataFormat="yyyy-MM-dd"),
+			@ExcelField(title="订单编号", attrName="saleOrderNo", align=Align.CENTER, sort=100),
+			@ExcelField(title="状态", attrName="state", align=Align.CENTER, sort=110, type=Type.EXPORT),
+			@ExcelField(title="产线", attrName="lineTitle", align=Align.CENTER, sort=120, type=Type.EXPORT),
+	})
+	public MesWorkOrder() {
+		this(null);
+	}
+
+	public MesWorkOrder(String id) {
+		super(id);
+	}
+
+	@NotBlank(message="工单号不能为空")
+	@Pattern(regexp="M.+", message="工单号必须以M开头")
+	@Size(min=0, max=64, message="工单号长度不能超过 64 个字符")
+	public String getWorkNo() {
+		return workNo;
+	}
+
+	public void setWorkNo(String workNo) {
+		this.workNo = workNo == null ? null : workNo.trim();
+	}
+
+	@Size(min=0, max=64, message="项目别长度不能超过 64 个字符")
+	public String getProjectCode() {
+		return projectCode;
+	}
+
+	public void setProjectCode(String projectCode) {
+		this.projectCode = projectCode == null ? null : projectCode.trim();
+	}
+
+	@NotBlank(message="总成料号不能为空")
+	@Pattern(regexp="1.+", message="总成料号必须以1开头")
+	@Size(min=0, max=64, message="总成料号长度不能超过 64 个字符")
+	public String getMaterialSn() {
+		return materialSn;
+	}
+
+	public void setMaterialSn(String materialSn) {
+		this.materialSn = materialSn == null ? null : materialSn.trim();
+	}
+
+	@Size(min=0, max=255, message="规格名称长度不能超过 255 个字符")
+	public String getSpecName() {
+		return specName;
+	}
+
+	public void setSpecName(String specName) {
+		this.specName = specName == null ? null : specName.trim();
+	}
+
+	@Size(min=0, max=128, message="图号长度不能超过 128 个字符")
+	public String getDrawingNo() {
+		return drawingNo;
+	}
+
+	public void setDrawingNo(String drawingNo) {
+		this.drawingNo = drawingNo == null ? null : drawingNo.trim();
+	}
+
+	@NotNull(message="需生产数量不能为空")
+	@Min(value=1, message="需生产数量必须是大于 0 的整数")
+	public Integer getPlanQty() {
+		return planQty;
+	}
+
+	public void setPlanQty(Integer planQty) {
+		this.planQty = planQty;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd")
+	@NotNull(message="开单日期不能为空")
+	public Date getOpenDate() {
+		return openDate;
+	}
+
+	public void setOpenDate(Date openDate) {
+		this.openDate = openDate;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd")
+	@NotNull(message="生产日期不能为空")
+	public Date getProduceDate() {
+		return produceDate;
+	}
+
+	public void setProduceDate(Date produceDate) {
+		this.produceDate = produceDate;
+	}
+
+	@Size(min=0, max=64, message="订单编号长度不能超过 64 个字符")
+	public String getSaleOrderNo() {
+		return saleOrderNo;
+	}
+
+	public void setSaleOrderNo(String saleOrderNo) {
+		this.saleOrderNo = saleOrderNo == null ? null : saleOrderNo.trim();
+	}
+
+	@Size(min=0, max=64, message="产线编号长度不能超过 64 个字符")
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn == null ? null : lineSn.trim();
+	}
+
+	public String getLineTitle() {
+		return lineTitle;
+	}
+
+	public void setLineTitle(String lineTitle) {
+		this.lineTitle = lineTitle;
+	}
+
+	@Size(min=0, max=64, message="工件类型长度不能超过 64 个字符")
+	public String getCateSn() {
+		return cateSn;
+	}
+
+	public void setCateSn(String cateSn) {
+		this.cateSn = cateSn == null ? null : cateSn.trim();
+	}
+
+	public String getCateTitle() {
+		return cateTitle;
+	}
+
+	public void setCateTitle(String cateTitle) {
+		this.cateTitle = cateTitle;
+	}
+
+	@Size(min=0, max=1, message="状态长度不能超过 1 个字符")
+	public String getState() {
+		return state;
+	}
+
+	public void setState(String state) {
+		this.state = state;
+	}
+
+	public Integer getDoneQty() {
+		return doneQty;
+	}
+
+	public void setDoneQty(Integer doneQty) {
+		this.doneQty = doneQty;
+	}
+
+	public Integer getRowIndex() {
+		return rowIndex;
+	}
+
+	public void setRowIndex(Integer rowIndex) {
+		this.rowIndex = rowIndex;
+	}
+
+	public Date getProduceDate_gte() {
+		return sqlMap.getWhere().getValue("produce_date", QueryType.GTE);
+	}
+
+	public void setProduceDate_gte(Date produceDate) {
+		sqlMap.getWhere().and("produce_date", QueryType.GTE, produceDate);
+	}
+
+	public Date getProduceDate_lte() {
+		return sqlMap.getWhere().getValue("produce_date", QueryType.LTE);
+	}
+
+	public void setProduceDate_lte(Date produceDate) {
+		sqlMap.getWhere().and("produce_date", QueryType.LTE, produceDate);
+	}
+
+	public Date getOpenDate_gte() {
+		return sqlMap.getWhere().getValue("open_date", QueryType.GTE);
+	}
+
+	public void setOpenDate_gte(Date openDate) {
+		sqlMap.getWhere().and("open_date", QueryType.GTE, openDate);
+	}
+
+	public Date getOpenDate_lte() {
+		return sqlMap.getWhere().getValue("open_date", QueryType.LTE);
+	}
+
+	public void setOpenDate_lte(Date openDate) {
+		sqlMap.getWhere().and("open_date", QueryType.LTE, openDate);
+	}
+}

+ 116 - 0
src/main/java/com/jeesite/modules/mes/entity/MesWorkOrderSn.java

@@ -0,0 +1,116 @@
+package com.jeesite.modules.mes.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jeesite.common.entity.DataEntity;
+
+import java.util.Date;
+
+/**
+ * 工单下的工件。数据来自上线记录上的工单号,不单独建表。
+ */
+public class MesWorkOrderSn extends DataEntity<MesWorkOrderSn> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String workNo;
+	private String sn;
+	private String cateSn;
+	private String cateTitle;
+	private String lineSn;
+	private String lineTitle;
+	private Date onlineDate;
+	/** 产品状态:0生产中 1合格 2返修中 3已报废 4待处理 */
+	private String productState;
+	private String currentOprno;
+	private String oprnoTitle;
+	private String stationResult;
+
+	public String getWorkNo() {
+		return workNo;
+	}
+
+	public void setWorkNo(String workNo) {
+		this.workNo = workNo;
+	}
+
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+
+	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 getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+
+	public String getLineTitle() {
+		return lineTitle;
+	}
+
+	public void setLineTitle(String lineTitle) {
+		this.lineTitle = lineTitle;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+	public Date getOnlineDate() {
+		return onlineDate;
+	}
+
+	public void setOnlineDate(Date onlineDate) {
+		this.onlineDate = onlineDate;
+	}
+
+	public String getProductState() {
+		return productState;
+	}
+
+	public void setProductState(String productState) {
+		this.productState = productState;
+	}
+
+	public String getCurrentOprno() {
+		return currentOprno;
+	}
+
+	public void setCurrentOprno(String currentOprno) {
+		this.currentOprno = currentOprno;
+	}
+
+	public String getOprnoTitle() {
+		return oprnoTitle;
+	}
+
+	public void setOprnoTitle(String oprnoTitle) {
+		this.oprnoTitle = oprnoTitle;
+	}
+
+	public String getStationResult() {
+		return stationResult;
+	}
+
+	public void setStationResult(String stationResult) {
+		this.stationResult = stationResult;
+	}
+}

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

@@ -79,6 +79,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	private MesProductService mesProductService;
 
 	@Autowired
+	private MesWorkOrderService mesWorkOrderService;
+
+	@Autowired
 	private MesProductQualityService mesProductQualityService;
 
 	@Autowired
@@ -329,6 +332,7 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			Integer i = 1;
 			Long cdate = Long.valueOf(CommonUitl.Date2TimeStamp(new Date()));
 			List<MesProductRecord> records = new ArrayList<>();
+			boolean workNoMarked = false;
 			for (MesTemplateItems mesTemplateItems : lists){
 				MesProductRecord mpr1 = new MesProductRecord();
 				mpr1.setSn(mesProductRecord.getSn());
@@ -353,6 +357,10 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 							break;
 						}
 					}
+					if(!workNoMarked && "OK".equals(mpr1.getContent()) && StringUtils.isNotBlank(mesProductRecord.getWorkNo())){
+						mpr1.setWorkNo(mesProductRecord.getWorkNo());
+						workNoMarked = true;
+					}
 				}
 
 				records.add(mpr1);
@@ -369,6 +377,9 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			mesProduct.setState("0");
 			mesProduct.setCate(cateSn);
 			mesProductService.save(mesProduct);
+			if(StringUtils.isNotBlank(mesProductRecord.getWorkNo())){
+				mesWorkOrderService.refreshDoneQty(mesProductRecord.getWorkNo());
+			}
 
 			return "RSOK";
 		}

+ 486 - 0
src/main/java/com/jeesite/modules/mes/service/MesWorkOrderService.java

@@ -0,0 +1,486 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelImport;
+import com.jeesite.modules.mes.dao.MesWorkOrderDao;
+import com.jeesite.modules.mes.entity.MesLine;
+import com.jeesite.modules.mes.entity.MesProductCate;
+import com.jeesite.modules.mes.entity.MesWorkOrder;
+import com.jeesite.modules.mes.entity.MesWorkOrderSn;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 工单Service。维护单据,并在上线时校验、回写完工数量。
+ * @author mes
+ * @version 2026-10-10
+ */
+@Service
+public class MesWorkOrderService extends CrudService<MesWorkOrderDao, MesWorkOrder> {
+
+	@Autowired
+	private MesLineService mesLineService;
+
+	@Autowired
+	private MesProductCateService mesProductCateService;
+
+	@Override
+	public MesWorkOrder get(MesWorkOrder mesWorkOrder) {
+		return super.get(mesWorkOrder);
+	}
+
+	@Override
+	public Page<MesWorkOrder> findPage(MesWorkOrder mesWorkOrder) {
+		return super.findPage(mesWorkOrder);
+	}
+
+	@Override
+	public List<MesWorkOrder> findList(MesWorkOrder mesWorkOrder) {
+		return super.findList(mesWorkOrder);
+	}
+
+	/**
+	 * 上线匹配结果。code 为空表示可以生产。
+	 */
+	public static class OnlineMatch {
+		private String code;
+		private MesWorkOrder order;
+
+		public boolean isOk() {
+			return code == null;
+		}
+
+		public String getCode() {
+			return code;
+		}
+
+		public MesWorkOrder getOrder() {
+			return order;
+		}
+	}
+
+	/**
+	 * 参数 mes.workOrder.check 为 1 时,上线校验工单并回写完工数量。
+	 */
+	public boolean isCheckEnabled() {
+		return "1".equals(Global.getConfig("mes.workOrder.check"));
+	}
+
+	/**
+	 * 按产线和工件类型匹配一张已下达或生产中的工单。
+	 * 开关关闭时不校验,order 为空。
+	 */
+	public OnlineMatch matchOnline(String lineSn, String sn, String lineCate) {
+		OnlineMatch match = new OnlineMatch();
+		if (!isCheckEnabled()) {
+			return match;
+		}
+		if (StringUtils.isBlank(lineSn)) {
+			match.code = "WO";
+			return match;
+		}
+		List<MesWorkOrder> orders = dao.findOnline(lineSn.trim());
+		if (orders == null || orders.isEmpty()) {
+			match.code = "WO";
+			return match;
+		}
+		boolean anyCate = false;
+		for (MesWorkOrder order : orders) {
+			if (StringUtils.isNotBlank(order.getCateSn())) {
+				anyCate = true;
+				break;
+			}
+		}
+		if (!anyCate) {
+			match.code = "WC";
+			return match;
+		}
+		String scannedSn = null;
+		if (StringUtils.isNotBlank(sn)) {
+			MesProductCate scanned = mesProductCateService.getCateBySn(sn.trim());
+			if (scanned != null) {
+				scannedSn = scanned.getSn();
+			}
+		}
+		List<MesWorkOrder> matched = new ArrayList<MesWorkOrder>();
+		for (MesWorkOrder order : orders) {
+			if (StringUtils.isBlank(order.getCateSn())) {
+				continue;
+			}
+			if (StringUtils.isNotBlank(scannedSn)) {
+				if (order.getCateSn().equalsIgnoreCase(scannedSn)) {
+					matched.add(order);
+				}
+			} else if (orders.size() == 1 || order.getCateSn().equalsIgnoreCase(lineCate == null ? "" : lineCate)) {
+				matched.add(order);
+			}
+		}
+		if (matched.isEmpty()) {
+			match.code = "WT";
+			return match;
+		}
+		MesWorkOrder order = matched.get(0);
+		int done = order.getDoneQty() == null ? 0 : order.getDoneQty();
+		int plan = order.getPlanQty() == null ? 0 : order.getPlanQty();
+		if (done >= plan) {
+			match.code = "WQ";
+			return match;
+		}
+		match.order = order;
+		return match;
+	}
+
+	@Transactional
+	public void refreshDoneQty(String workNo) {
+		if (StringUtils.isBlank(workNo)) {
+			return;
+		}
+		MesWorkOrder order = findByWorkNo(workNo);
+		if (order == null || !MesWorkOrder.STATUS_NORMAL.equals(order.getStatus())) {
+			return;
+		}
+		if (!MesWorkOrder.STATE_ISSUED.equals(order.getState()) && !MesWorkOrder.STATE_PRODUCING.equals(order.getState())) {
+			return;
+		}
+		int done = dao.countOnlineOk(workNo);
+		order.setDoneQty(done);
+		if (order.getPlanQty() != null && done >= order.getPlanQty()) {
+			order.setState(MesWorkOrder.STATE_DONE);
+		} else if (MesWorkOrder.STATE_ISSUED.equals(order.getState())) {
+			order.setState(MesWorkOrder.STATE_PRODUCING);
+		}
+		dao.updateDone(order);
+	}
+
+	public Page<MesWorkOrderSn> findSnPage(MesWorkOrderSn query) {
+		Page<MesWorkOrderSn> page = query.getPage();
+		page.setList(dao.findSnList(query));
+		return page;
+	}
+
+	public MesWorkOrder findByWorkNo(String workNo) {
+		if (StringUtils.isBlank(workNo)) {
+			return null;
+		}
+		return dao.findByWorkNo(workNo);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesWorkOrder order) {
+		normalize(order);
+		if (order.getIsNewRecord()) {
+			insertFromForm(order);
+			return;
+		}
+		updateFromForm(order);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesWorkOrder order) {
+		MesWorkOrder old = get(order);
+		if (old == null) {
+			throw new ServiceException(text("工单不存在"));
+		}
+		if (!MesWorkOrder.STATE_DRAFT.equals(old.getState())) {
+			throw new ServiceException(text("只有待下达的工单可以删除"));
+		}
+		super.delete(old);
+	}
+
+	@Transactional
+	public void issue(MesWorkOrder order) {
+		MesWorkOrder old = required(order);
+		if (!MesWorkOrder.STATE_DRAFT.equals(old.getState())) {
+			throw new ServiceException(text("只有待下达的工单可以下达"));
+		}
+		if (StringUtils.isBlank(old.getLineSn())) {
+			throw new ServiceException(text("请先选择产线再下达"));
+		}
+		if (StringUtils.isBlank(old.getCateSn())) {
+			throw new ServiceException(text("请先选择工件类型再下达"));
+		}
+		checkLine(old.getLineSn());
+		checkCate(old.getCateSn());
+		old.setState(MesWorkOrder.STATE_ISSUED);
+		super.save(old);
+	}
+
+	@Transactional
+	public void recall(MesWorkOrder order) {
+		MesWorkOrder old = required(order);
+		if (!MesWorkOrder.STATE_ISSUED.equals(old.getState())) {
+			throw new ServiceException(text("只有已下达的工单可以撤回"));
+		}
+		old.setState(MesWorkOrder.STATE_DRAFT);
+		super.save(old);
+	}
+
+	@Transactional
+	public void close(MesWorkOrder order) {
+		MesWorkOrder old = required(order);
+		if (!MesWorkOrder.STATE_DRAFT.equals(old.getState()) && !MesWorkOrder.STATE_ISSUED.equals(old.getState())) {
+			throw new ServiceException(text("当前状态不能关闭"));
+		}
+		old.setState(MesWorkOrder.STATE_CLOSED);
+		super.save(old);
+	}
+
+	/**
+	 * 样表第 1 行是说明,第 2 行是表头,数据从第 3 行开始。
+	 */
+	@Transactional
+	public String importData(MultipartFile file) {
+		if (file == null) {
+			throw new ServiceException(text("请选择导入的数据文件!"));
+		}
+		int successNum = 0;
+		int failureNum = 0;
+		StringBuilder successMsg = new StringBuilder();
+		StringBuilder failureMsg = new StringBuilder();
+		Set<String> seen = new HashSet<String>();
+		try (ExcelImport ei = new ExcelImport(file, 2, 0)) {
+			List<MesWorkOrder> list = ei.getDataList(MesWorkOrder.class);
+			for (MesWorkOrder order : list) {
+				normalize(order);
+				if (isBlankRow(order)) {
+					continue;
+				}
+				String label = StringUtils.defaultIfBlank(order.getWorkNo(), "空工单号");
+				try {
+					if (StringUtils.isNotBlank(order.getWorkNo()) && !seen.add(order.getWorkNo())) {
+						throw new ServiceException(text("文件中工单号重复"));
+					}
+					saveImported(order);
+					successNum++;
+					successMsg.append("<br/>").append(successNum).append("、工单 ").append(label).append(" 导入成功");
+				} catch (Exception e) {
+					failureNum++;
+					failureMsg.append("<br/>").append(failureNum).append("、工单 ").append(label).append(" 导入失败:")
+							.append(e.getMessage());
+					logger.error(e.getMessage(), e);
+				}
+			}
+		} catch (ServiceException e) {
+			throw e;
+		} catch (Exception e) {
+			logger.error(e.getMessage(), e);
+			throw new ServiceException(e.getMessage());
+		}
+		if (successNum == 0 && failureNum == 0) {
+			throw new ServiceException(text("没有可导入的数据"));
+		}
+		if (failureNum > 0) {
+			failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据不正确,错误如下:");
+			throw new ServiceException(failureMsg.toString());
+		}
+		successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+		return successMsg.toString();
+	}
+
+	private void insertFromForm(MesWorkOrder order) {
+		validateCore(order);
+		checkLine(order.getLineSn());
+		checkCate(order.getCateSn());
+		MesWorkOrder exist = findByWorkNo(order.getWorkNo());
+		if (exist != null && !MesWorkOrder.STATUS_DELETE.equals(exist.getStatus())) {
+			throw new ServiceException(text("工单号已存在"));
+		}
+		if (exist != null) {
+			revive(exist, order, true);
+			return;
+		}
+		order.setState(MesWorkOrder.STATE_DRAFT);
+		order.setDoneQty(0);
+		super.save(order);
+	}
+
+	private void updateFromForm(MesWorkOrder order) {
+		MesWorkOrder old = get(order);
+		if (old == null) {
+			throw new ServiceException(text("工单不存在"));
+		}
+		if (!StringUtils.equals(old.getWorkNo(), order.getWorkNo())) {
+			throw new ServiceException(text("工单号不能修改"));
+		}
+		if (MesWorkOrder.STATE_DRAFT.equals(old.getState())) {
+			validateCore(order);
+			checkLine(order.getLineSn());
+			checkCate(order.getCateSn());
+			order.setState(old.getState());
+			order.setDoneQty(old.getDoneQty() == null ? 0 : old.getDoneQty());
+			super.save(order);
+			return;
+		}
+		if (MesWorkOrder.STATE_ISSUED.equals(old.getState())) {
+			if (StringUtils.isBlank(order.getLineSn())) {
+				throw new ServiceException(text("已下达工单不能清空产线"));
+			}
+			checkLine(order.getLineSn());
+			if (order.getOpenDate() == null || order.getProduceDate() == null) {
+				throw new ServiceException(text("开单日期和生产日期不能为空"));
+			}
+			old.setLineSn(order.getLineSn());
+			old.setOpenDate(order.getOpenDate());
+			old.setProduceDate(order.getProduceDate());
+			old.setSaleOrderNo(order.getSaleOrderNo());
+			old.setRemarks(order.getRemarks());
+			super.save(old);
+			return;
+		}
+		throw new ServiceException(text("当前状态不能修改"));
+	}
+
+	private void saveImported(MesWorkOrder incoming) {
+		validateCore(incoming);
+		MesWorkOrder exist = findByWorkNo(incoming.getWorkNo());
+		if (exist == null) {
+			incoming.setIsNewRecord(true);
+			incoming.setState(MesWorkOrder.STATE_DRAFT);
+			incoming.setDoneQty(0);
+			incoming.setLineSn(null);
+			super.save(incoming);
+			return;
+		}
+		if (MesWorkOrder.STATUS_DELETE.equals(exist.getStatus())) {
+			revive(exist, incoming, false);
+			return;
+		}
+		if (!MesWorkOrder.STATE_DRAFT.equals(exist.getState())) {
+			throw new ServiceException(text("不是待下达,不能通过导入覆盖"));
+		}
+		copySchedule(exist, incoming);
+		super.save(exist);
+	}
+
+	private void revive(MesWorkOrder exist, MesWorkOrder incoming, boolean fromForm) {
+		exist.setStatus(MesWorkOrder.STATUS_NORMAL);
+		updateStatus(exist);
+		if (fromForm) {
+			copySchedule(exist, incoming);
+			exist.setLineSn(incoming.getLineSn());
+			exist.setRemarks(incoming.getRemarks());
+		} else {
+			copySchedule(exist, incoming);
+			exist.setLineSn(null);
+			exist.setRemarks(null);
+		}
+		exist.setState(MesWorkOrder.STATE_DRAFT);
+		exist.setDoneQty(0);
+		super.save(exist);
+	}
+
+	private void copySchedule(MesWorkOrder target, MesWorkOrder source) {
+		target.setProjectCode(source.getProjectCode());
+		target.setMaterialSn(source.getMaterialSn());
+		target.setSpecName(source.getSpecName());
+		target.setDrawingNo(source.getDrawingNo());
+		target.setPlanQty(source.getPlanQty());
+		target.setOpenDate(source.getOpenDate());
+		target.setProduceDate(source.getProduceDate());
+		target.setSaleOrderNo(source.getSaleOrderNo());
+	}
+
+	private MesWorkOrder required(MesWorkOrder order) {
+		MesWorkOrder old = get(order);
+		if (old == null) {
+			throw new ServiceException(text("工单不存在"));
+		}
+		return old;
+	}
+
+	private void validateCore(MesWorkOrder order) {
+		if (StringUtils.isBlank(order.getWorkNo()) || !order.getWorkNo().matches("M.+")) {
+			throw new ServiceException(text("工单号必须以M开头"));
+		}
+		if (order.getWorkNo().length() > 64) {
+			throw new ServiceException(text("工单号长度不能超过 64 个字符"));
+		}
+		if (StringUtils.isBlank(order.getMaterialSn()) || !order.getMaterialSn().matches("1.+")) {
+			throw new ServiceException(text("总成料号必须以1开头"));
+		}
+		if (order.getMaterialSn().length() > 64) {
+			throw new ServiceException(text("总成料号长度不能超过 64 个字符"));
+		}
+		if (order.getPlanQty() == null || order.getPlanQty() < 1) {
+			throw new ServiceException(text("需生产数量必须是大于 0 的整数"));
+		}
+		if (order.getOpenDate() == null) {
+			throw new ServiceException(text("开单日期不能为空"));
+		}
+		if (order.getProduceDate() == null) {
+			throw new ServiceException(text("生产日期不能为空"));
+		}
+	}
+
+	private void checkLine(String lineSn) {
+		if (StringUtils.isBlank(lineSn)) {
+			return;
+		}
+		MesLine query = new MesLine();
+		query.setSn(lineSn);
+		query.setStatus(MesLine.STATUS_NORMAL);
+		MesLine line = mesLineService.findInfo(query);
+		if (line == null) {
+			throw new ServiceException(text("产线不存在:" + lineSn));
+		}
+	}
+
+	private void checkCate(String cateSn) {
+		if (StringUtils.isBlank(cateSn)) {
+			return;
+		}
+		MesProductCate cate = mesProductCateService.findBySn(cateSn);
+		if (cate == null || !MesProductCate.STATUS_NORMAL.equals(cate.getStatus())) {
+			throw new ServiceException(text("工件类型不存在:" + cateSn));
+		}
+	}
+
+	private void normalize(MesWorkOrder order) {
+		order.setWorkNo(plain(order.getWorkNo()));
+		order.setProjectCode(plain(order.getProjectCode()));
+		order.setMaterialSn(plain(order.getMaterialSn()));
+		order.setDrawingNo(plain(order.getDrawingNo()));
+		order.setSaleOrderNo(plain(order.getSaleOrderNo()));
+		order.setLineSn(plain(order.getLineSn()));
+		order.setCateSn(plain(order.getCateSn()));
+		if (order.getSpecName() != null) {
+			order.setSpecName(order.getSpecName().trim());
+		}
+	}
+
+	private String plain(String value) {
+		if (value == null) {
+			return null;
+		}
+		String text = value.trim();
+		if (text.matches("\\d+\\.0+")) {
+			return text.substring(0, text.indexOf('.'));
+		}
+		return text;
+	}
+
+	private boolean isBlankRow(MesWorkOrder order) {
+		return StringUtils.isBlank(order.getWorkNo())
+				&& StringUtils.isBlank(order.getProjectCode())
+				&& StringUtils.isBlank(order.getMaterialSn())
+				&& StringUtils.isBlank(order.getSpecName())
+				&& StringUtils.isBlank(order.getDrawingNo())
+				&& (order.getPlanQty() == null || order.getPlanQty() == 0)
+				&& order.getOpenDate() == null
+				&& order.getProduceDate() == null
+				&& StringUtils.isBlank(order.getSaleOrderNo());
+	}
+}

+ 25 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -144,6 +144,9 @@ public class MesProductRecordController extends BaseController {
 	private MesLineService mesLineService;
 
 	@Autowired
+	private MesWorkOrderService mesWorkOrderService;
+
+	@Autowired
 	private MesLineProcessService mesLineProcessService;
 
 	@Autowired
@@ -757,6 +760,14 @@ public class MesProductRecordController extends BaseController {
 			}
 		}
 
+		MesWorkOrderService.OnlineMatch match = mesWorkOrderService.matchOnline(lineSn, sn, cateSn);
+		if(!match.isOk()){
+			return "RS" + match.getCode();
+		}
+		if(match.getOrder() != null){
+			cateSn = match.getOrder().getCateSn();
+		}
+
 		MesProductRecord mpr = mesProductRecordService.findBySnCate(sn,cateSn);
 		if(!ObjectUtils.isEmpty(mpr)){
 			return "RSCF";  // 工件码重复
@@ -769,6 +780,9 @@ public class MesProductRecordController extends BaseController {
 		mesProductRecord.setContent(retokng);
 		mesProductRecord.setCreateBy(user);
 		mesProductRecord.setCateSn(cateSn);
+		if(match.getOrder() != null){
+			mesProductRecord.setWorkNo(match.getOrder().getWorkNo());
+		}
 
 		// 质量上传,解析参数
 		List<ParamsResp> params = mesProductRecordService.getQualityParams(oprno,msg);
@@ -859,6 +873,14 @@ public class MesProductRecordController extends BaseController {
 			}
 		}
 
+		MesWorkOrderService.OnlineMatch match = mesWorkOrderService.matchOnline(lineSn, sn, cateSn);
+		if(!match.isOk()){
+			return "RS" + match.getCode();
+		}
+		if(match.getOrder() != null){
+			cateSn = match.getOrder().getCateSn();
+		}
+
 		MesProductRecord mpr = mesProductRecordService.findBySnCate(sn,cateSn);
 		if(!ObjectUtils.isEmpty(mpr)){
 			return "RSCF";  // 工件码重复
@@ -871,6 +893,9 @@ public class MesProductRecordController extends BaseController {
 		mesProductRecord.setContent(retokng);
 		mesProductRecord.setCreateBy(user);
 		mesProductRecord.setCateSn(cateSn);
+		if(match.getOrder() != null){
+			mesProductRecord.setWorkNo(match.getOrder().getWorkNo());
+		}
 
 		// 质量上传,解析参数
 		List<ParamsResp> params = new ArrayList<ParamsResp>();

+ 188 - 0
src/main/java/com/jeesite/modules/mes/web/MesWorkOrderController.java

@@ -0,0 +1,188 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.DateUtils;
+import com.jeesite.common.utils.excel.ExcelExport;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesLine;
+import com.jeesite.modules.mes.entity.MesProductCate;
+import com.jeesite.modules.mes.entity.MesWorkOrder;
+import com.jeesite.modules.mes.entity.MesWorkOrderSn;
+import com.jeesite.modules.mes.service.MesLineService;
+import com.jeesite.modules.mes.service.MesProductCateService;
+import com.jeesite.modules.mes.service.MesWorkOrderService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工单Controller。只维护单据,不改现场上线。
+ * @author mes
+ * @version 2026-10-10
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesWorkOrder")
+public class MesWorkOrderController extends BaseController {
+
+	@Autowired
+	private MesWorkOrderService mesWorkOrderService;
+
+	@Autowired
+	private MesLineService mesLineService;
+
+	@Autowired
+	private MesProductCateService mesProductCateService;
+
+	@ModelAttribute
+	public MesWorkOrder get(String id, boolean isNewRecord) {
+		return mesWorkOrderService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesWorkOrder mesWorkOrder, Model model) {
+		model.addAttribute("mesWorkOrder", mesWorkOrder);
+		model.addAttribute("lines", distinctLines());
+		model.addAttribute("cates", productCates());
+		return "modules/mes/mesWorkOrderList";
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesWorkOrder> listData(MesWorkOrder mesWorkOrder, HttpServletRequest request, HttpServletResponse response) {
+		mesWorkOrder.setPage(new Page<>(request, response));
+		return mesWorkOrderService.findPage(mesWorkOrder);
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "snList")
+	public String snList(MesWorkOrderSn mesWorkOrderSn, Model model) {
+		model.addAttribute("mesWorkOrderSn", mesWorkOrderSn);
+		return "modules/mes/mesWorkOrderSnList";
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "snListData")
+	@ResponseBody
+	public Page<MesWorkOrderSn> snListData(MesWorkOrderSn mesWorkOrderSn, HttpServletRequest request, HttpServletResponse response) {
+		mesWorkOrderSn.setPage(new Page<>(request, response));
+		return mesWorkOrderService.findSnPage(mesWorkOrderSn);
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "form")
+	public String form(MesWorkOrder mesWorkOrder, Model model) {
+		model.addAttribute("mesWorkOrder", mesWorkOrder);
+		model.addAttribute("lines", distinctLines());
+		model.addAttribute("cates", productCates());
+		return "modules/mes/mesWorkOrderForm";
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.save(mesWorkOrder);
+		return renderResult(Global.TRUE, text("保存工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.delete(mesWorkOrder);
+		return renderResult(Global.TRUE, text("删除工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "issue")
+	@ResponseBody
+	public String issue(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.issue(mesWorkOrder);
+		return renderResult(Global.TRUE, text("下达工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "recall")
+	@ResponseBody
+	public String recall(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.recall(mesWorkOrder);
+		return renderResult(Global.TRUE, text("撤回工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@RequestMapping(value = "close")
+	@ResponseBody
+	public String close(MesWorkOrder mesWorkOrder) {
+		mesWorkOrderService.close(mesWorkOrder);
+		return renderResult(Global.TRUE, text("关闭工单成功!"));
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "exportData")
+	public void exportData(MesWorkOrder mesWorkOrder, HttpServletResponse response) {
+		List<MesWorkOrder> list = mesWorkOrderService.findList(mesWorkOrder);
+		String fileName = "工单" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+		try (ExcelExport ee = new ExcelExport("工单排程", MesWorkOrder.class)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	@RequiresPermissions("mes:mesWorkOrder:view")
+	@RequestMapping(value = "importTemplate")
+	public void importTemplate(HttpServletResponse response) {
+		MesWorkOrder sample = new MesWorkOrder();
+		List<MesWorkOrder> list = ListUtils.newArrayList(sample);
+		String fileName = "武汉厂工单排程表模板.xlsx";
+		try (ExcelExport ee = new ExcelExport("工单排程", MesWorkOrder.class, Type.IMPORT)) {
+			ee.setDataList(list).write(response, fileName);
+		}
+	}
+
+	private List<MesLine> distinctLines() {
+		Map<String, MesLine> lines = new LinkedHashMap<String, MesLine>();
+		for (MesLine line : mesLineService.findList(new MesLine())) {
+			if (line != null && StringUtils.isNotBlank(line.getSn()) && !lines.containsKey(line.getSn())) {
+				lines.put(line.getSn(), line);
+			}
+		}
+		return new ArrayList<MesLine>(lines.values());
+	}
+
+	private List<MesProductCate> productCates() {
+		MesProductCate query = new MesProductCate();
+		query.setCate(0);
+		return mesProductCateService.findList(query);
+	}
+
+	@ResponseBody
+	@RequiresPermissions("mes:mesWorkOrder:edit")
+	@PostMapping(value = "importData")
+	public String importData(MultipartFile file) {
+		try {
+			String message = mesWorkOrderService.importData(file);
+			return renderResult(Global.TRUE, "posfull:" + message);
+		} catch (Exception ex) {
+			return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
+		}
+	}
+}

+ 8 - 0
src/main/java/com/jeesite/modules/utils/ErrorMsg.java

@@ -72,6 +72,14 @@ public class ErrorMsg {
                 lmsg = "上一工位完成后冷却时间未到";
             } else if (processMsgRet.equalsIgnoreCase("RP")) {
                 lmsg = "该精追码已在本工位加工完成,不可重复";
+            } else if (processMsgRet.equalsIgnoreCase("WO")) {
+                lmsg = "当前产线没有已下达或生产中的工单";
+            } else if (processMsgRet.equalsIgnoreCase("WT")) {
+                lmsg = "工件类型与当前工单不一致";
+            } else if (processMsgRet.equalsIgnoreCase("WQ")) {
+                lmsg = "工单计划数量已完成";
+            } else if (processMsgRet.equalsIgnoreCase("WC")) {
+                lmsg = "工单未选择工件类型";
             }
         }catch (Exception e){ }
         return lmsg;

+ 141 - 0
src/main/resources/mappings/modules/mes/MesWorkOrderDao.xml

@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.mes.dao.MesWorkOrderDao">
+
+	<select id="findByWorkNo" resultType="com.jeesite.modules.mes.entity.MesWorkOrder">
+		SELECT
+			a.id AS "id",
+			a.work_no AS "workNo",
+			a.project_code AS "projectCode",
+			a.material_sn AS "materialSn",
+			a.spec_name AS "specName",
+			a.drawing_no AS "drawingNo",
+			a.plan_qty AS "planQty",
+			a.open_date AS "openDate",
+			a.produce_date AS "produceDate",
+			a.sale_order_no AS "saleOrderNo",
+			a.line_sn AS "lineSn",
+			a.cate_sn AS "cateSn",
+			a.state AS "state",
+			a.done_qty AS "doneQty",
+			a.status AS "status",
+			a.remarks AS "remarks"
+		FROM mes_work_order a
+		WHERE a.work_no = #{workNo}
+		LIMIT 1
+	</select>
+
+	<select id="findOnline" resultType="com.jeesite.modules.mes.entity.MesWorkOrder">
+		SELECT
+			a.id AS "id",
+			a.work_no AS "workNo",
+			a.material_sn AS "materialSn",
+			a.plan_qty AS "planQty",
+			a.line_sn AS "lineSn",
+			a.cate_sn AS "cateSn",
+			a.state AS "state",
+			a.done_qty AS "doneQty",
+			a.status AS "status"
+		FROM mes_work_order a
+		WHERE a.line_sn = #{lineSn}
+			AND a.status = '0'
+			AND a.state IN ('1', '2')
+		ORDER BY a.state DESC, a.produce_date ASC, a.create_date ASC
+	</select>
+
+	<select id="countOnlineOk" resultType="int">
+		SELECT COUNT(1)
+		FROM mes_product_record
+		WHERE work_no = #{workNo}
+			AND content = 'OK'
+	</select>
+
+	<select id="findSnList" resultType="com.jeesite.modules.mes.entity.MesWorkOrderSn">
+		SELECT
+			x.id AS "id",
+			x.workNo AS "workNo",
+			x.sn AS "sn",
+			x.cateSn AS "cateSn",
+			x.cateTitle AS "cateTitle",
+			x.lineSn AS "lineSn",
+			x.lineTitle AS "lineTitle",
+			x.onlineDate AS "onlineDate",
+			x.productState AS "productState",
+			x.currentOprno AS "currentOprno",
+			x.stationResult AS "stationResult",
+			(
+				SELECT lp.title
+				FROM mes_line_process lp
+				INNER JOIN mes_line l ON l.id = lp.line_id AND l.status = '0'
+				WHERE lp.status = '0'
+					AND lp.oprno = x.currentOprno
+					AND l.sn = x.lineSn
+				ORDER BY lp.id
+				LIMIT 1
+			) AS "oprnoTitle"
+		FROM (
+			SELECT
+				r.id AS id,
+				r.work_no AS workNo,
+				r.sn AS sn,
+				r.cate_sn AS cateSn,
+				c.title AS cateTitle,
+				r.line_sn AS lineSn,
+				ln.title AS lineTitle,
+				r.create_date AS onlineDate,
+				(
+					SELECT p.state
+					FROM mes_product p
+					WHERE p.sn = r.sn
+					ORDER BY p.update_date DESC
+					LIMIT 1
+				) AS productState,
+				(
+					SELECT t.oprno
+					FROM mes_product_record t
+					WHERE t.sn = r.sn
+						AND t.craft = '100000'
+						AND t.content IN ('OK', 'NG')
+					ORDER BY t.update_date DESC, t.id DESC
+					LIMIT 1
+				) AS currentOprno,
+				(
+					SELECT t.content
+					FROM mes_product_record t
+					WHERE t.sn = r.sn
+						AND t.craft = '100000'
+						AND t.content IN ('OK', 'NG')
+					ORDER BY t.update_date DESC, t.id DESC
+					LIMIT 1
+				) AS stationResult
+			FROM mes_product_record r
+			LEFT JOIN mes_product_cate c ON c.sn = r.cate_sn AND c.status = '0'
+			LEFT JOIN mes_line ln ON ln.sn = r.line_sn AND ln.status = '0'
+				AND ln.id = (
+					SELECT MIN(l2.id) FROM mes_line l2 WHERE l2.sn = r.line_sn AND l2.status = '0'
+				)
+			WHERE r.work_no IS NOT NULL AND r.work_no != ''
+			<if test="workNo != null and workNo != ''">
+				AND r.work_no LIKE CONCAT('%', #{workNo}, '%')
+			</if>
+			<if test="sn != null and sn != ''">
+				AND r.sn LIKE CONCAT('%', #{sn}, '%')
+			</if>
+		) x
+		<where>
+			<if test="productState != null and productState != ''">
+				AND x.productState = #{productState}
+			</if>
+		</where>
+		ORDER BY x.onlineDate DESC, x.sn ASC
+	</select>
+
+	<update id="updateDone">
+		UPDATE mes_work_order
+		SET done_qty = #{doneQty},
+			state = #{state},
+			update_date = NOW()
+		WHERE id = #{id}
+	</update>
+
+</mapper>

+ 11 - 0
src/main/resources/static/screen/kpi-screen.css

@@ -60,6 +60,17 @@ body {
 	line-height: 32px;
 }
 
+.demo-toggle {
+	cursor: pointer;
+	font-family: inherit;
+}
+
+.demo-toggle.is-on {
+	color: #ffd56a;
+	border-color: rgba(255, 184, 0, 0.75);
+	background: rgba(92, 58, 0, 0.72);
+}
+
 .screen-title-new {
 	color: #fff;
 	font-size: 36px;

+ 94 - 2
src/main/resources/static/screen/kpi-screen.js

@@ -288,13 +288,99 @@
 		renderProducts(data.products);
 	}
 
+	var demoOn = false;
+
+	function statDayText(date) {
+		var day = new Date(date.getTime());
+		if (day.getHours() < 8) {
+			day.setDate(day.getDate() - 1);
+		}
+		return day.getFullYear() + "-" + pad(day.getMonth() + 1) + "-" + pad(day.getDate());
+	}
+
+	function trendLabel(date, offset) {
+		var day = new Date(date.getTime());
+		if (day.getHours() < 8) {
+			day.setDate(day.getDate() - 1);
+		}
+		day.setDate(day.getDate() - offset);
+		return (day.getMonth() + 1) + "/" + day.getDate();
+	}
+
+	function buildDemoData() {
+		var now = new Date();
+		var labels = [];
+		for (var i = 6; i >= 0; i--) {
+			labels.push(trendLabel(now, i));
+		}
+		return {
+			day: statDayText(now),
+			window: "08:00-次日08:00",
+			shiftTitle: "白班",
+			summary: { plan: 480, actual: 436, ok: 860, ng: 28, repair: 11, openAlarm: 1 },
+			kpi: {
+				achieve: 90.8,
+				fpy: 96.8,
+				oee: 85.4,
+				availability: 97.2,
+				performance: 90.8,
+				quality: 96.8,
+				shiftAchieve: 94.2
+			},
+			hours: {
+				labels: ["08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00", "22:00", "00:00", "02:00", "04:00", "06:00"],
+				actual: [28, 42, 38, 44, 46, 47, 36, 40, 38, 34, 28, 15],
+				plan: [40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40]
+			},
+			trend: {
+				labels: labels,
+				achieve: [88.2, 91.0, 86.4, 93.1, 89.5, 92.4, 90.8],
+				fpy: [96.1, 97.2, 95.4, 97.8, 96.6, 97.0, 96.8],
+				oee: [82.1, 85.4, 80.6, 88.2, 84.0, 86.7, 85.4]
+			},
+			shifts: [
+				{ title: "白班", range: "08:00-20:00", plan: 260, actual: 245, ok: 470, ng: 12, repair: 5, achieve: 94.2, fpy: 97.5, current: true },
+				{ title: "夜班", range: "20:00-08:00", plan: 220, actual: 191, ok: 390, ng: 16, repair: 6, achieve: 86.8, fpy: 96.1, current: false }
+			],
+			products: [
+				{ title: "冷板总成", plan: 180, actual: 172, achieve: 95.6 },
+				{ title: "框架总成", plan: 160, actual: 146, achieve: 91.3 },
+				{ title: "箱体总成", plan: 140, actual: 118, achieve: 84.3 }
+			]
+		};
+	}
+
+	function syncDemoButton() {
+		$("#demoToggle").toggleClass("is-on", demoOn).text(demoOn ? "退出演示" : "演示模式");
+	}
+
+	function setDemo(on) {
+		demoOn = !!on;
+		syncDemoButton();
+		if (refreshTimer) {
+			clearTimeout(refreshTimer);
+			refreshTimer = null;
+		}
+		if (demoOn) {
+			applyData(buildDemoData());
+			return;
+		}
+		loadData();
+	}
+
 	function loadData() {
+		if (demoOn) {
+			return;
+		}
 		$.ajax({
 			url: config.apiUrl,
 			type: "GET",
 			dataType: "json",
 			timeout: 20000
 		}).done(function (res) {
+			if (demoOn) {
+				return;
+			}
 			if (res && (res.result === "true" || res.result === true) && res.data) {
 				applyData(res.data);
 				scheduleRefresh(config.refreshInterval || 60000);
@@ -302,7 +388,9 @@
 				scheduleRefresh(5000);
 			}
 		}).fail(function () {
-			scheduleRefresh(5000);
+			if (!demoOn) {
+				scheduleRefresh(5000);
+			}
 		});
 	}
 
@@ -335,7 +423,11 @@
 		$("#screenTitle").text(config.title || "生产运营看板");
 		updateClock();
 		setInterval(updateClock, 1000);
-		loadData();
+		$("#demoToggle").on("click", function () {
+			setDemo(!demoOn);
+		});
+		var query = window.location.search || "";
+		setDemo(/(?:^|[?&])demo=1(?:&|$)/.test(query));
 		fitScreen();
 		$(window).on("resize", fitScreen);
 	});

+ 3 - 2
src/main/resources/views/modules/mes/mesScreen10.html

@@ -5,7 +5,7 @@
 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 	<meta http-equiv="X-UA-Compatible" content="ie=edge">
 	<title>生产运营看板</title>
-	<link rel="stylesheet" href="${ctxStatic}/screen/kpi-screen.css?v=20260930">
+	<link rel="stylesheet" href="${ctxStatic}/screen/kpi-screen.css?v=20261010">
 </head>
 <body>
 <div class="screen-page">
@@ -15,6 +15,7 @@
 			<div class="header-meta">
 				<span class="meta-chip" id="statDay">统计日 --</span>
 				<span class="meta-chip" id="statWindow">08:00-次日08:00</span>
+				<button type="button" class="meta-chip demo-toggle" id="demoToggle">演示模式</button>
 			</div>
 			<div class="screen-title-new" id="screenTitle">生产运营看板</div>
 			<div class="screen-time" id="screenTime"></div>
@@ -144,7 +145,7 @@
 		apiUrl: "${ctx}/mes/mesProduct/screenKpiData"
 	};
 </script>
-<script src="${ctxStatic}/screen/kpi-screen.js?v=20260930"></script>
+<script src="${ctxStatic}/screen/kpi-screen.js?v=20261010"></script>
 <script src="${ctxStatic}/screen/screen-fullscreen.js?v=20261010"></script>
 </body>
 </html>

+ 187 - 0
src/main/resources/views/modules/mes/mesWorkOrderForm.html

@@ -0,0 +1,187 @@
+<% layout('/layouts/default.html', {title: '工单管理', libs: ['validate']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-note"></i> ${text(mesWorkOrder.isNewRecord ? '新增工单' : '编辑工单')}
+			</div>
+			<div class="box-tools pull-right">
+				<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
+			</div>
+		</div>
+		<%
+		var stateLabel = '待下达';
+		if(mesWorkOrder.state == '1'){
+			stateLabel = '已下达';
+		}else if(mesWorkOrder.state == '2'){
+			stateLabel = '生产中';
+		}else if(mesWorkOrder.state == '3'){
+			stateLabel = '完工';
+		}else if(mesWorkOrder.state == '4'){
+			stateLabel = '暂停';
+		}else if(mesWorkOrder.state == '5'){
+			stateLabel = '关闭';
+		}
+		%>
+		<#form:form id="inputForm" model="${mesWorkOrder}" action="${ctx}/mes/mesWorkOrder/save" method="post" class="form-horizontal">
+			<div class="box-body">
+				<div class="form-unit">${text('基本信息')}</div>
+				<#form:hidden path="id"/>
+				<div class="row">
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('工单号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="workNo" maxlength="64" class="form-control required" placeholder="M开头"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('项目别')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="projectCode" maxlength="64" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('总成料号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="materialSn" maxlength="64" class="form-control required" placeholder="1开头"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('规格名称')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="specName" maxlength="255" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('图号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="drawingNo" maxlength="128" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('需生产数量')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="planQty" maxlength="8" class="form-control required digits"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('开单日期')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="openDate" readonly="true" maxlength="20" class="form-control laydate required"
+									dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								<span class="required">*</span> ${text('生产日期')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="produceDate" readonly="true" maxlength="20" class="form-control laydate required"
+									dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('订单编号')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="saleOrderNo" maxlength="64" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('产线')}:</label>
+							<div class="col-sm-8">
+								<#form:select path="lineSn" items="${lines}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('工件类型')}:</label>
+							<div class="col-sm-8">
+								<#form:select path="cateSn" items="${cates}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('状态')}:</label>
+							<div class="col-sm-8">
+								<input type="text" class="form-control" readonly value="${stateLabel}"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4">
+								${text('备注')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="remarks" maxlength="500" class="form-control"/>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesWorkOrder:edit')){ %>
+							<button type="submit" class="btn btn-sm btn-primary" id="btnSubmit"><i class="fa fa-check"></i> ${text('保 存')}</button>&nbsp;
+						<% } %>
+						<button type="button" class="btn btn-sm btn-default" id="btnCancel" onclick="js.closeCurrentTabPage()"><i class="fa fa-reply-all"></i> ${text('关 闭')}</button>
+					</div>
+				</div>
+			</div>
+		</#form:form>
+	</div>
+</div>
+<% } %>
+<script>
+<% if(!mesWorkOrder.isNewRecord && mesWorkOrder.state != '0'){ %>
+	$('#workNo,#projectCode,#materialSn,#specName,#drawingNo,#planQty,#cateSn').attr('readonly', 'readonly');
+	$('#cateSn').attr('disabled', 'disabled');
+<% } %>
+<% if(!mesWorkOrder.isNewRecord && mesWorkOrder.state != '0' && mesWorkOrder.state != '1'){ %>
+	$('#inputForm').find('input,select,textarea').attr('readonly', 'readonly').attr('disabled', 'disabled');
+	$('#btnSubmit').hide();
+<% } %>
+$("#inputForm").validate({
+	submitHandler: function(form){
+		js.ajaxSubmitForm($(form), function(data){
+			js.showMessage(data.message);
+			if(data.result == Global.TRUE){
+				js.closeCurrentTabPage(function(contentWindow){
+					contentWindow.page();
+				});
+			}
+		}, "json");
+    }
+});
+</script>

+ 190 - 0
src/main/resources/views/modules/mes/mesWorkOrderList.html

@@ -0,0 +1,190 @@
+<% layout('/layouts/default.html', {title: '工单管理', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('工单管理')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> ${text('导出')}</a>
+				<% if(hasPermi('mes:mesWorkOrder:edit')){ %>
+					<a href="#" class="btn btn-default" id="btnImport"><i class="glyphicon glyphicon-import"></i> ${text('导入')}</a>
+					<a href="${ctx}/mes/mesWorkOrder/form" class="btn btn-default btnTool" title="${text('新增工单')}"><i class="fa fa-plus"></i> ${text('新增')}</a>
+				<% } %>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesWorkOrder}" action="${ctx}/mes/mesWorkOrder/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<label class="control-label">${text('工单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="workNo" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('总成料号')}:</label>
+					<div class="control-inline">
+						<#form:input path="materialSn" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('项目别')}:</label>
+					<div class="control-inline">
+						<#form:input path="projectCode" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('订单编号')}:</label>
+					<div class="control-inline">
+						<#form:input path="saleOrderNo" maxlength="64" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('产线')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="lineSn" items="${lines}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件类型')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="cateSn" items="${cates}" itemLabel="title" itemValue="sn" blankOption="true" class="form-control"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('状态')}:</label>
+					<div class="control-inline width-120">
+						<select name="state" class="form-control">
+							<option value="">${text('全部')}</option>
+							<option value="0"<% if(mesWorkOrder.state == '0'){ %> selected<% } %>>${text('待下达')}</option>
+							<option value="1"<% if(mesWorkOrder.state == '1'){ %> selected<% } %>>${text('已下达')}</option>
+							<option value="2"<% if(mesWorkOrder.state == '2'){ %> selected<% } %>>${text('生产中')}</option>
+							<option value="3"<% if(mesWorkOrder.state == '3'){ %> selected<% } %>>${text('完工')}</option>
+							<option value="4"<% if(mesWorkOrder.state == '4'){ %> selected<% } %>>${text('暂停')}</option>
+							<option value="5"<% if(mesWorkOrder.state == '5'){ %> selected<% } %>>${text('关闭')}</option>
+						</select>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('生产日期')}:</label>
+					<div class="control-inline">
+						<#form:input path="produceDate_gte" readonly="true" maxlength="20" class="form-control laydate width-date"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+						&nbsp;-&nbsp;
+						<#form:input path="produceDate_lte" readonly="true" maxlength="20" class="form-control laydate width-date"
+							dataFormat="date" data-type="date" data-format="yyyy-MM-dd"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+var workOrderStateMap = {'0':'待下达','1':'已下达','2':'生产中','3':'完工','4':'暂停','5':'关闭'};
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("工单号")}', name:'workNo', index:'a.work_no', width:150, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesWorkOrder/form?id='+row.id+'" class="btnList" data-title="${text("编辑工单")}">'+(val||'')+'</a>';
+		}},
+		{header:'${text("项目别")}', name:'projectCode', index:'a.project_code', width:90, align:"center"},
+		{header:'${text("总成料号")}', name:'materialSn', index:'a.material_sn', width:140, align:"center"},
+		{header:'${text("规格名称")}', name:'specName', index:'a.spec_name', width:160, align:"left"},
+		{header:'${text("图号")}', name:'drawingNo', index:'a.drawing_no', width:120, align:"center"},
+		{header:'${text("需生产数量")}', name:'planQty', index:'a.plan_qty', width:100, align:"center"},
+		{header:'${text("已完工")}', name:'doneQty', index:'a.done_qty', width:80, align:"center"},
+		{header:'${text("开单日期")}', name:'openDate', index:'a.open_date', width:110, align:"center"},
+		{header:'${text("生产日期")}', name:'produceDate', index:'a.produce_date', width:110, align:"center"},
+		{header:'${text("订单编号")}', name:'saleOrderNo', index:'a.sale_order_no', width:140, align:"center"},
+		{header:'${text("产线")}', name:'lineTitle', index:'a.line_sn', width:120, align:"center"},
+		{header:'${text("工件类型")}', name:'cateTitle', index:'a.cate_sn', width:120, align:"center"},
+		{header:'${text("状态")}', name:'state', index:'a.state', width:90, align:"center", formatter: function(val, obj, row, act){
+			return workOrderStateMap[val] || val || '';
+		}},
+		{header:'${text("操作")}', name:'actions', width:190, formatter: function(val, obj, row, act){
+			var actions = [];
+			actions.push('<a href="${ctx}/mes/mesWorkOrder/snList?workNo='+(row.workNo||'')+'" class="addTabPage" data-title="${text("工单工件")}" title="${text("工件")}"><i class="fa fa-list"></i></a>&nbsp;');
+			<% if(hasPermi('mes:mesWorkOrder:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesWorkOrder/form?id='+row.id+'" class="btnList" title="${text("编辑")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				if (row.state == '0'){
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/issue?id='+row.id+'" class="btnList" title="${text("下达")}" data-confirm="${text("确认下达该工单吗?")}"><i class="glyphicon glyphicon-ok"></i></a>&nbsp;');
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/delete?id='+row.id+'" class="btnList" title="${text("删除")}" data-confirm="${text("确认删除该工单吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+				}
+				if (row.state == '1'){
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/recall?id='+row.id+'" class="btnList" title="${text("撤回")}" data-confirm="${text("确认撤回该工单吗?")}"><i class="glyphicon glyphicon-arrow-left"></i></a>&nbsp;');
+				}
+				if (row.state == '0' || row.state == '1'){
+					actions.push('<a href="${ctx}/mes/mesWorkOrder/close?id='+row.id+'" class="btnList" title="${text("关闭")}" data-confirm="${text("确认关闭该工单吗?")}"><i class="glyphicon glyphicon-ban-circle"></i></a>&nbsp;');
+				}
+			<% } %>
+			return actions.join('');
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>
+<script>
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), {
+		url:'${ctx}/mes/mesWorkOrder/exportData',
+		downloadFile:true
+	});
+});
+$('#btnImport').click(function(){
+	js.layer.open({
+		type: 1,
+		area: ['420px'],
+		title: '${text("导入工单")}',
+		resize: false,
+		scrollbar: true,
+		content: js.template('importTpl'),
+		btn: ['<i class="fa fa-check"></i> ${text("导入")}',
+			'<i class="fa fa-remove"></i> ${text("关闭")}'],
+		btn1: function(index, layero){
+			var form = {
+				inputForm: layero.find('#inputForm'),
+				file: layero.find('#file').val()
+			};
+		    if (form.file == '' || (!js.endWith(form.file, '.xls') && !js.endWith(form.file, '.xlsx'))){
+		    	js.showMessage("${text('文件不正确,请选择后缀为“xls”或“xlsx”的文件。')}", null, 'warning');
+		        return false;
+		    }
+			js.ajaxSubmitForm(form.inputForm, function(data){
+				js.showMessage(data.message);
+				if(data.result == Global.TRUE){
+					js.layer.closeAll();
+				}
+				page();
+			}, "json");
+			return true;
+		}
+	});
+});
+</script>
+<script id="importTpl" type="text/template">//<!--
+<form id="inputForm" action="${ctx}/mes/mesWorkOrder/importData" method="post" enctype="multipart/form-data"
+	class="form-horizontal mt20 mb10" style="overflow:auto;max-height:220px;">
+	<div class="row">
+		<div class="col-xs-12 col-xs-offset-1">
+			<input type="file" id="file" name="file" class="form-file"/>
+			<div class="mt10 pt5" style="color:red">
+				${text('可直接导入武汉厂工单排程表。第1行说明、第2行表头、第3行起为数据。')}
+			</div>
+			<div class="mt10 pt5">
+				<a href="${ctx}/mes/mesWorkOrder/importTemplate" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> ${text('下载模板')}</a>
+			</div>
+		</div>
+	</div>
+</form>
+//--></script>

+ 72 - 0
src/main/resources/views/modules/mes/mesWorkOrderSnList.html

@@ -0,0 +1,72 @@
+<% layout('/layouts/default.html', {title: '工单工件', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('工单工件')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesWorkOrderSn}" action="${ctx}/mes/mesWorkOrder/snListData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<label class="control-label">${text('工单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="workNo" maxlength="64" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件码')}:</label>
+					<div class="control-inline">
+						<#form:input path="sn" maxlength="100" class="form-control width-260"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件状态')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="productState" dictType="mes_product_status" blankOption="true" class="form-control"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+var stationResultMap = {'OK':'合格','NG':'不合格'};
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("工单号")}', name:'workNo', width:150, align:"center", frozen:true},
+		{header:'${text("工件码")}', name:'sn', width:280, align:"center", frozen:true, formatter: function(val, obj, row, act){
+			return '<a href="${ctx}/mes/mesProductRecord/list?sn='+(val||'')+'" class="addTabPage" data-title="${text("生产记录")}">'+(val||'')+'</a>';
+		}},
+		{header:'${text("工件类型")}', name:'cateTitle', width:120, align:"center"},
+		{header:'${text("产线")}', name:'lineTitle', width:140, align:"center"},
+		{header:'${text("上线时间")}', name:'onlineDate', width:160, align:"center"},
+		{header:'${text("工件状态")}', name:'productState', width:100, align:"center", formatter: function(val, obj, row, act){
+			return js.getDictLabel(${@DictUtils.getDictListJson('mes_product_status')}, val, '${text("未知")}', true);
+		}},
+		{header:'${text("最近工位")}', name:'oprnoTitle', width:180, align:"center", formatter: function(val, obj, row, act){
+			if (!row.currentOprno){
+				return '${text("未加工")}';
+			}
+			return (val || '') + (row.currentOprno ? '('+row.currentOprno+')' : '');
+		}},
+		{header:'${text("工位结果")}', name:'stationResult', width:90, align:"center", formatter: function(val, obj, row, act){
+			return stationResultMap[val] || val || '';
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>