Просмотр исходного кода

优化产品生产记录页面等

jingbo 2 месяцев назад
Родитель
Сommit
2afde73a3f
44 измененных файлов с 2424 добавлено и 148 удалено
  1. 8 0
      api.http
  2. 14 0
      src/main/java/com/jeesite/modules/mes/dao/MesLineProcessConsumeDao.java
  3. 25 0
      src/main/java/com/jeesite/modules/mes/dao/MesMaterialKcTotalDao.java
  4. 14 0
      src/main/java/com/jeesite/modules/mes/dao/MesProcessConsumeLogDao.java
  5. 15 0
      src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java
  6. 168 0
      src/main/java/com/jeesite/modules/mes/entity/MesLineProcessConsume.java
  7. 2 3
      src/main/java/com/jeesite/modules/mes/entity/MesLineProcessMaterial.java
  8. 100 0
      src/main/java/com/jeesite/modules/mes/entity/MesMaterialKcTotal.java
  9. 189 0
      src/main/java/com/jeesite/modules/mes/entity/MesProcessConsumeLog.java
  10. 8 7
      src/main/java/com/jeesite/modules/mes/entity/MesProductArcWelding.java
  11. 13 0
      src/main/java/com/jeesite/modules/mes/entity/MesProductRecord.java
  12. 52 0
      src/main/java/com/jeesite/modules/mes/service/MesLineProcessConsumeService.java
  13. 53 3
      src/main/java/com/jeesite/modules/mes/service/MesMaterialKcLogService.java
  14. 153 0
      src/main/java/com/jeesite/modules/mes/service/MesMaterialKcTotalService.java
  15. 40 0
      src/main/java/com/jeesite/modules/mes/service/MesProcessConsumeLogService.java
  16. 255 0
      src/main/java/com/jeesite/modules/mes/service/MesProcessConsumeStockService.java
  17. 35 0
      src/main/java/com/jeesite/modules/mes/service/MesProductRecordService.java
  18. 23 0
      src/main/java/com/jeesite/modules/mes/web/MesBatchSnController.java
  19. 130 0
      src/main/java/com/jeesite/modules/mes/web/MesLineProcessConsumeController.java
  20. 3 7
      src/main/java/com/jeesite/modules/mes/web/MesLineProcessMaterialController.java
  21. 81 0
      src/main/java/com/jeesite/modules/mes/web/MesMaterialKcTotalController.java
  22. 57 0
      src/main/java/com/jeesite/modules/mes/web/MesProcessConsumeLogController.java
  23. 13 0
      src/main/java/com/jeesite/modules/mes/web/MesProductArcWeldingController.java
  24. 124 17
      src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java
  25. 3 3
      src/main/java/com/jeesite/modules/mes/web/MesScrapController.java
  26. 2 0
      src/main/java/com/jeesite/modules/utils/ErrorMsg.java
  27. 3 0
      src/main/resources/config/application.yml
  28. 4 0
      src/main/resources/mappings/modules/mes/MesLineProcessConsumeDao.xml
  29. 27 0
      src/main/resources/mappings/modules/mes/MesMaterialKcTotalDao.xml
  30. 4 0
      src/main/resources/mappings/modules/mes/MesProcessConsumeLogDao.xml
  31. 50 0
      src/main/resources/mappings/modules/mes/MesProductRecordDao.xml
  32. 21 2
      src/main/resources/views/modules/mes/mesBatchSnList.html
  33. 69 0
      src/main/resources/views/modules/mes/mesBatchSnQrcode.html
  34. 108 0
      src/main/resources/views/modules/mes/mesLineProcessConsumeForm.html
  35. 90 0
      src/main/resources/views/modules/mes/mesLineProcessConsumeList.html
  36. 4 1
      src/main/resources/views/modules/mes/mesLineProcessList.html
  37. 70 0
      src/main/resources/views/modules/mes/mesMaterialKcTotalList.html
  38. 11 10
      src/main/resources/views/modules/mes/mesMaterialSelect.html
  39. 102 0
      src/main/resources/views/modules/mes/mesProcessConsumeLogList.html
  40. 1 1
      src/main/resources/views/modules/mes/mesProductArcWeldingForm.html
  41. 18 7
      src/main/resources/views/modules/mes/mesProductArcWeldingList.html
  42. 189 0
      src/main/resources/views/modules/mes/mesProductRecordDetailList.html
  43. 55 80
      src/main/resources/views/modules/mes/mesProductRecordList.html
  44. 18 7
      src/main/resources/views/modules/mes/mesProductSpotWeldingList.html

+ 8 - 0
api.http

@@ -108,6 +108,7 @@ Content-Type: application/x-www-form-urlencoded
 __ajax=json&__sid=34157dcaeba34e71a617792b0d2e9589&id=1684766376594120704
 
 ###
+# PDA 功能模块(返回格式由参数 mes.icons.grouped 控制:0=扁平 1=分组)
 POST http://127.0.0.1:8980/js/a/mes/mesIcons/icons
 Content-Type: application/x-www-form-urlencoded
 
@@ -357,6 +358,13 @@ Content-Type: application/json
 
 ###
 
+POST http://127.0.0.1:8980/js/a/mes/mesDevice/reportState
+Content-Type: application/json
+
+{"gw":"OP040A","lineSn":"115XT","state":"1","content":"工位登录,开始运行"}
+
+###
+
 POST http://183.62.68.218:8001/dataChange/all/outCheck
 Content-Type: application/x-www-form-urlencoded
 

+ 14 - 0
src/main/java/com/jeesite/modules/mes/dao/MesLineProcessConsumeDao.java

@@ -0,0 +1,14 @@
+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.MesLineProcessConsume;
+
+/**
+ * 工位加工消耗物料绑定 DAO
+ * @author mes
+ * @version 2026-07-14
+ */
+@MyBatisDao
+public interface MesLineProcessConsumeDao extends CrudDao<MesLineProcessConsume> {
+}

+ 25 - 0
src/main/java/com/jeesite/modules/mes/dao/MesMaterialKcTotalDao.java

@@ -0,0 +1,25 @@
+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.MesMaterialKcTotal;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 物料总库存DAO接口
+ * @author mes
+ * @version 2026-07-14
+ */
+@MyBatisDao
+public interface MesMaterialKcTotalDao extends CrudDao<MesMaterialKcTotal> {
+
+	/**
+	 * 按物料查询汇总记录
+	 */
+	MesMaterialKcTotal findByMaterialId(@Param("materialId") String materialId);
+
+	/**
+	 * 按物料汇总明细库存数量(来自 mes_material_kc_log)
+	 */
+	Long sumNumsByMaterialId(@Param("materialId") String materialId);
+}

+ 14 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProcessConsumeLogDao.java

@@ -0,0 +1,14 @@
+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.MesProcessConsumeLog;
+
+/**
+ * 加工消耗扣料日志 DAO
+ * @author mes
+ * @version 2026-07-14
+ */
+@MyBatisDao
+public interface MesProcessConsumeLogDao extends CrudDao<MesProcessConsumeLog> {
+}

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductRecordDao.java

@@ -29,4 +29,19 @@ public interface MesProductRecordDao extends CrudDao<MesProductRecord> {
 
     @Update("update mes_product_record set content = #{content} where craft=#{craft} and sn=#{sn} and line_sn=#{lineSn} and oprno=#{oprno}")
     Boolean updateResultByCraftAndSnAndLineSnAndOprno(MesProductRecord mesProductRecord);
+
+	/**
+	 * 按工件码分组列表(分页)
+	 */
+	List<MesProductRecord> findSnGroupList(MesProductRecord mesProductRecord);
+
+	/**
+	 * 按工件码分组总数
+	 */
+	long findSnGroupCount(MesProductRecord mesProductRecord);
+
+	/**
+	 * 指定工件下的工位聚合列表
+	 */
+	List<MesProductRecord> findOprnoGroupList(MesProductRecord mesProductRecord);
 }

+ 168 - 0
src/main/java/com/jeesite/modules/mes/entity/MesLineProcessConsume.java

@@ -0,0 +1,168 @@
+package com.jeesite.modules.mes.entity;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.lang.StringUtils;
+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 javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+/**
+ * 工位加工消耗物料绑定 Entity
+ * @author mes
+ * @version 2026-07-14
+ */
+@Table(name = "mes_line_process_consume", alias = "a", label = "工位加工消耗物料绑定", columns = {
+		@Column(name = "id", attrName = "id", label = "编号", isPK = true),
+		@Column(name = "line_process_id", attrName = "lineProcessId", label = "工位"),
+		@Column(name = "material_id", attrName = "materialId", label = "物料"),
+		@Column(name = "nums", attrName = "nums", label = "每次加工消耗数量", isUpdateForce = true),
+		@Column(name = "status", attrName = "status", label = "状态"),
+		@Column(name = "create_by", attrName = "createBy", label = "create_by", isUpdate = false, isQuery = false),
+		@Column(name = "create_date", attrName = "createDate", label = "create_date", isUpdate = false, isQuery = false, isUpdateForce = true),
+		@Column(name = "update_by", attrName = "updateBy", label = "update_by", isQuery = false),
+		@Column(name = "update_date", attrName = "updateDate", label = "update_date", isQuery = false, isUpdateForce = true),
+		@Column(name = "remarks", attrName = "remarks", label = "备注", isQuery = false),
+}, joinTable = {
+		@JoinTable(type = JoinTable.Type.JOIN, entity = MesLineProcess.class, attrName = "this", alias = "b",
+				on = "b.id = a.line_process_id",
+				columns = {
+						@Column(name = "title", attrName = "processTitle", label = "工序名称"),
+						@Column(name = "oprno", attrName = "oprno", label = "工位号"),
+				}),
+		@JoinTable(type = JoinTable.Type.JOIN, entity = MesMaterial.class, attrName = "this", alias = "c",
+				on = "c.id = a.material_id",
+				columns = {
+						@Column(name = "title", attrName = "materialTitle", label = "物料名称"),
+						@Column(name = "type", attrName = "type", label = "追溯类型"),
+						@Column(name = "erp_sn", attrName = "erpSn", label = "ERP料号"),
+				}),
+		@JoinTable(type = JoinTable.Type.JOIN, entity = MesLine.class, attrName = "this", alias = "d",
+				on = "d.id = b.line_id",
+				columns = {
+						@Column(name = "sn", attrName = "lineSn", label = "产线编号"),
+				}),
+		@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = MesMaterialKcTotal.class, attrName = "this", alias = "t",
+				on = "t.material_id = a.material_id",
+				columns = {
+						@Column(name = "total_nums", attrName = "totalNums", label = "当前总库存"),
+				}),
+}, orderBy = "a.update_date DESC")
+public class MesLineProcessConsume extends DataEntity<MesLineProcessConsume> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String lineProcessId;
+	private String materialId;
+	/** 每次加工消耗数量 */
+	private Integer nums;
+	private String processTitle;
+	private String materialTitle;
+	private String erpSn;
+	private String oprno;
+	private String lineSn;
+	private String type;
+	/** 当前总库存(来自 mes_material_kc_total) */
+	private Long totalNums;
+
+	public MesLineProcessConsume() {
+		this(null);
+	}
+
+	public MesLineProcessConsume(String id) {
+		super(id);
+	}
+
+	@NotBlank(message = "工位不能为空")
+	@Size(min = 0, max = 64, message = "工位长度不能超过 64 个字符")
+	public String getLineProcessId() {
+		return lineProcessId;
+	}
+
+	public void setLineProcessId(String lineProcessId) {
+		this.lineProcessId = lineProcessId;
+	}
+
+	@NotBlank(message = "物料不能为空")
+	@Size(min = 0, max = 64, message = "物料长度不能超过 64 个字符")
+	public String getMaterialId() {
+		return materialId;
+	}
+
+	public void setMaterialId(String materialId) {
+		this.materialId = materialId;
+	}
+
+	public Integer getNums() {
+		return nums;
+	}
+
+	public void setNums(Integer nums) {
+		this.nums = nums;
+	}
+
+	public String getProcessTitle() {
+		return processTitle;
+	}
+
+	public void setProcessTitle(String processTitle) {
+		this.processTitle = processTitle;
+	}
+
+	public String getMaterialTitle() {
+		return materialTitle;
+	}
+
+	public void setMaterialTitle(String materialTitle) {
+		this.materialTitle = materialTitle;
+		if (StringUtils.isNotBlank(materialTitle)) {
+			sqlMap.getWhere().and("c.title", QueryType.LIKE, materialTitle.trim());
+		}
+	}
+
+	public String getErpSn() {
+		return erpSn;
+	}
+
+	public void setErpSn(String erpSn) {
+		this.erpSn = erpSn;
+		if (StringUtils.isNotBlank(erpSn)) {
+			sqlMap.getWhere().and("c.erp_sn", QueryType.LIKE, erpSn.trim());
+		}
+	}
+
+	public String getOprno() {
+		return oprno;
+	}
+
+	public void setOprno(String oprno) {
+		this.oprno = oprno;
+	}
+
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public Long getTotalNums() {
+		return totalNums;
+	}
+
+	public void setTotalNums(Long totalNums) {
+		this.totalNums = totalNums;
+	}
+}

+ 2 - 3
src/main/java/com/jeesite/modules/mes/entity/MesLineProcessMaterial.java

@@ -7,10 +7,9 @@ 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;
 
 /**
- * 生产工序物料表Entity
+ * 生产工序物料表Entity(旧版绑定,客户端物料防错仍在使用)
  * @author mes
  * @version 2024-04-02
  */
@@ -154,4 +153,4 @@ public class MesLineProcessMaterial extends DataEntity<MesLineProcessMaterial> {
 	public void setErpSn(String erpSn) {
 		this.erpSn = erpSn;
 	}
-}
+}

+ 100 - 0
src/main/java/com/jeesite/modules/mes/entity/MesMaterialKcTotal.java

@@ -0,0 +1,100 @@
+package com.jeesite.modules.mes.entity;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.lang.StringUtils;
+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 javax.validation.constraints.Size;
+
+/**
+ * 物料总库存(按 material_id 汇总)Entity
+ * @author mes
+ * @version 2026-07-14
+ */
+@Table(name = "mes_material_kc_total", alias = "a", label = "物料总库存", columns = {
+		@Column(name = "id", attrName = "id", label = "编号", isPK = true),
+		@Column(name = "material_id", attrName = "materialId", label = "物料"),
+		@Column(name = "total_nums", attrName = "totalNums", label = "总库存", isUpdateForce = true),
+		@Column(name = "status", attrName = "status", label = "状态", isUpdate = false),
+		@Column(name = "create_by", attrName = "createBy", label = "create_by", isUpdate = false, isQuery = false),
+		@Column(name = "create_date", attrName = "createDate", label = "创建时间", isUpdate = false, isQuery = false, isUpdateForce = true),
+		@Column(name = "update_by", attrName = "updateBy", label = "update_by", isQuery = false),
+		@Column(name = "update_date", attrName = "updateDate", label = "更新时间", isQuery = false, isUpdateForce = true),
+		@Column(name = "remarks", attrName = "remarks", label = "备注", isQuery = false),
+}, joinTable = {
+		@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = MesMaterial.class, attrName = "this", alias = "m",
+				on = "m.id = a.material_id",
+				columns = {
+						@Column(name = "title", attrName = "materialTitle", label = "物料名称"),
+						@Column(name = "erp_sn", attrName = "erpSn", label = "ERP料号"),
+						@Column(name = "spec", attrName = "spec", label = "规格"),
+				}),
+}, orderBy = "a.total_nums DESC, a.update_date DESC")
+public class MesMaterialKcTotal extends DataEntity<MesMaterialKcTotal> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String materialId;
+	private Long totalNums;
+	private String materialTitle;
+	private String erpSn;
+	private String spec;
+
+	public MesMaterialKcTotal() {
+		this(null);
+	}
+
+	public MesMaterialKcTotal(String id) {
+		super(id);
+	}
+
+	@Size(min = 0, max = 64, message = "物料长度不能超过 64 个字符")
+	public String getMaterialId() {
+		return materialId;
+	}
+
+	public void setMaterialId(String materialId) {
+		this.materialId = materialId;
+	}
+
+	public Long getTotalNums() {
+		return totalNums;
+	}
+
+	public void setTotalNums(Long totalNums) {
+		this.totalNums = totalNums;
+	}
+
+	public String getMaterialTitle() {
+		return materialTitle;
+	}
+
+	public void setMaterialTitle(String materialTitle) {
+		this.materialTitle = materialTitle;
+		if (StringUtils.isNotBlank(materialTitle)) {
+			sqlMap.getWhere().and("m.title", QueryType.LIKE, materialTitle.trim());
+		}
+	}
+
+	public String getErpSn() {
+		return erpSn;
+	}
+
+	public void setErpSn(String erpSn) {
+		this.erpSn = erpSn;
+		if (StringUtils.isNotBlank(erpSn)) {
+			sqlMap.getWhere().and("m.erp_sn", QueryType.LIKE, erpSn.trim());
+		}
+	}
+
+	public String getSpec() {
+		return spec;
+	}
+
+	public void setSpec(String spec) {
+		this.spec = spec;
+	}
+}

+ 189 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProcessConsumeLog.java

@@ -0,0 +1,189 @@
+package com.jeesite.modules.mes.entity;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.lang.StringUtils;
+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 java.util.Date;
+
+/**
+ * 加工消耗扣料日志 Entity
+ * @author mes
+ * @version 2026-07-14
+ */
+@Table(name = "mes_process_consume_log", alias = "a", label = "加工消耗扣料日志", columns = {
+		@Column(name = "id", attrName = "id", label = "编号", isPK = true),
+		@Column(name = "sn", attrName = "sn", label = "工件码", queryType = QueryType.LIKE),
+		@Column(name = "oprno", attrName = "oprno", label = "工位号", queryType = QueryType.LIKE),
+		@Column(name = "line_sn", attrName = "lineSn", label = "产线编号", queryType = QueryType.LIKE),
+		@Column(name = "parent_process_id", attrName = "parentProcessId", label = "父工位"),
+		@Column(name = "material_id", attrName = "materialId", label = "物料"),
+		@Column(name = "nums", attrName = "nums", label = "扣减数量", isUpdateForce = true),
+		@Column(name = "batch_sn", attrName = "batchSn", label = "批次码", queryType = QueryType.LIKE),
+		@Column(name = "kc_log_id", attrName = "kcLogId", label = "库存明细ID"),
+		@Column(name = "status", attrName = "status", label = "状态"),
+		@Column(name = "create_by", attrName = "createBy", label = "操作人", isUpdate = false),
+		@Column(name = "create_date", attrName = "createDate", label = "扣料时间", isUpdate = false, isUpdateForce = true),
+		@Column(name = "update_by", attrName = "updateBy", label = "update_by", isQuery = false),
+		@Column(name = "update_date", attrName = "updateDate", label = "update_date", isQuery = false, isUpdateForce = true),
+		@Column(name = "remarks", attrName = "remarks", label = "备注", isQuery = false),
+}, joinTable = {
+		@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = MesMaterial.class, attrName = "this", alias = "m",
+				on = "m.id = a.material_id",
+				columns = {
+						@Column(name = "title", attrName = "materialTitle", label = "物料名称"),
+						@Column(name = "erp_sn", attrName = "erpSn", label = "ERP料号"),
+				}),
+		@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = MesLineProcess.class, attrName = "this", alias = "p",
+				on = "p.id = a.parent_process_id",
+				columns = {
+						@Column(name = "title", attrName = "parentProcessTitle", label = "父工位名称"),
+						@Column(name = "oprno", attrName = "parentOprno", label = "父工位号"),
+				}),
+}, orderBy = "a.create_date DESC")
+public class MesProcessConsumeLog extends DataEntity<MesProcessConsumeLog> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String sn;
+	private String oprno;
+	private String lineSn;
+	private String parentProcessId;
+	private String materialId;
+	private Integer nums;
+	private String batchSn;
+	private String kcLogId;
+	private String materialTitle;
+	private String erpSn;
+	private String parentProcessTitle;
+	private String parentOprno;
+
+	public MesProcessConsumeLog() {
+		this(null);
+	}
+
+	public MesProcessConsumeLog(String id) {
+		super(id);
+	}
+
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+
+	public String getOprno() {
+		return oprno;
+	}
+
+	public void setOprno(String oprno) {
+		this.oprno = oprno;
+	}
+
+	public String getLineSn() {
+		return lineSn;
+	}
+
+	public void setLineSn(String lineSn) {
+		this.lineSn = lineSn;
+	}
+
+	public String getParentProcessId() {
+		return parentProcessId;
+	}
+
+	public void setParentProcessId(String parentProcessId) {
+		this.parentProcessId = parentProcessId;
+	}
+
+	public String getMaterialId() {
+		return materialId;
+	}
+
+	public void setMaterialId(String materialId) {
+		this.materialId = materialId;
+	}
+
+	public Integer getNums() {
+		return nums;
+	}
+
+	public void setNums(Integer nums) {
+		this.nums = nums;
+	}
+
+	public String getBatchSn() {
+		return batchSn;
+	}
+
+	public void setBatchSn(String batchSn) {
+		this.batchSn = batchSn;
+	}
+
+	public String getKcLogId() {
+		return kcLogId;
+	}
+
+	public void setKcLogId(String kcLogId) {
+		this.kcLogId = kcLogId;
+	}
+
+	public String getMaterialTitle() {
+		return materialTitle;
+	}
+
+	public void setMaterialTitle(String materialTitle) {
+		this.materialTitle = materialTitle;
+		if (StringUtils.isNotBlank(materialTitle)) {
+			sqlMap.getWhere().and("m.title", QueryType.LIKE, materialTitle.trim());
+		}
+	}
+
+	public String getErpSn() {
+		return erpSn;
+	}
+
+	public void setErpSn(String erpSn) {
+		this.erpSn = erpSn;
+		if (StringUtils.isNotBlank(erpSn)) {
+			sqlMap.getWhere().and("m.erp_sn", QueryType.LIKE, erpSn.trim());
+		}
+	}
+
+	public String getParentProcessTitle() {
+		return parentProcessTitle;
+	}
+
+	public void setParentProcessTitle(String parentProcessTitle) {
+		this.parentProcessTitle = parentProcessTitle;
+	}
+
+	public String getParentOprno() {
+		return parentOprno;
+	}
+
+	public void setParentOprno(String parentOprno) {
+		this.parentOprno = parentOprno;
+	}
+
+	public Date getCreateDate_gte() {
+		return sqlMap.getWhere().getValue("create_date", QueryType.GTE);
+	}
+
+	public void setCreateDate_gte(Date createDate) {
+		sqlMap.getWhere().and("create_date", QueryType.GTE, createDate);
+	}
+
+	public Date getCreateDate_lte() {
+		return sqlMap.getWhere().getValue("create_date", QueryType.LTE);
+	}
+
+	public void setCreateDate_lte(Date createDate) {
+		sqlMap.getWhere().and("create_date", QueryType.LTE, createDate);
+	}
+}

+ 8 - 7
src/main/java/com/jeesite/modules/mes/entity/MesProductArcWelding.java

@@ -22,27 +22,28 @@ import com.jeesite.common.utils.excel.annotation.ExcelFields;
 		@Column(name="id", attrName="id", label="id", isPK=true),
 		@Column(name="line_sn", attrName="lineSn", label="产线编号", queryType=QueryType.LIKE),
 		@Column(name="oprno", attrName="oprno", label="工位号", queryType=QueryType.LIKE),
-		@Column(name="sn", attrName="sn", label="二维码", queryType=QueryType.LIKE),
+		@Column(name="sn", attrName="sn", label="工件码", queryType=QueryType.LIKE),
 		@Column(name="content", attrName="content", label="参数内容", isQuery=false),
-		@Column(name="create_date", attrName="createDate", label="创建时间", isPK=true),
+		@Column(name="create_date", attrName="createDate", label="创建时间", isPK=true, isUpdate=false, isQuery=false),
 	},
 	joinTable={
 		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLine.class, attrName="this", alias="c",
 			on="c.sn = a.line_sn AND c.status = '0'",
 			columns={}),
 		@JoinTable(type=JoinTable.Type.LEFT_JOIN, entity=MesLineProcess.class, attrName="this", alias="b",
-			on="b.line_id = c.id AND b.status = '0' AND (b.oprno = a.oprno OR (CHAR_LENGTH(a.oprno) = 6 AND b.oprno = LEFT(a.oprno, 5)))",
+			// 仅精确匹配工位号,避免同时命中父工位(OP070)与子工位(OP070A)导致列表翻倍
+			on="b.line_id = c.id AND b.status = '0' AND b.oprno = a.oprno",
 			columns={
 				@Column(name="title", attrName="oprnoTitle", label="工位名称"),
 			}),
-	}, orderBy="a.id DESC, a.create_date DESC"
+	}, orderBy="a.create_date DESC, a.id DESC"
 )
 public class MesProductArcWelding extends DataEntity<MesProductArcWelding> {
 	
 	private static final long serialVersionUID = 1L;
 	private String lineSn;		// 产线编号
 	private String oprno;		// 工位号
-	private String sn;		// 二维码
+	private String sn;		// 工件码
 	private String content;		// 参数内容
 	private String oprnoTitle;	// 工位名称(关联工序表,支持模糊查询)
 	private String weldingCategory;	// 焊接类型:spot-点焊,arc-弧焊
@@ -52,7 +53,7 @@ public class MesProductArcWelding extends DataEntity<MesProductArcWelding> {
 		@ExcelField(title="产线编号", attrName="lineSn", align=Align.CENTER, sort=20),
 		@ExcelField(title="工位号", attrName="oprno", align=Align.CENTER, sort=30),
 		@ExcelField(title="工位名称", attrName="oprnoTitle", align=Align.CENTER, sort=35),
-		@ExcelField(title="二维码", attrName="sn", align=Align.CENTER, sort=40),
+		@ExcelField(title="工件码", attrName="sn", align=Align.CENTER, sort=40),
 		@ExcelField(title="参数内容", attrName="content", align=Align.CENTER, sort=50),
 		@ExcelField(title="创建时间", attrName="createDate", align=Align.CENTER, sort=60),
 	})
@@ -83,7 +84,7 @@ public class MesProductArcWelding extends DataEntity<MesProductArcWelding> {
 		this.oprno = oprno;
 	}
 	
-	@Size(min=0, max=50, message="二维码长度不能超过 50 个字符")
+	@Size(min=0, max=50, message="工件码长度不能超过 50 个字符")
 	public String getSn() {
 		return sn;
 	}

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

@@ -72,6 +72,8 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 	private Integer treeLevel;		// 工位号
 	private String treeCode;
 	private String parentCode;
+	/** 树表叶子:1=明细行,0=聚合行(列表懒加载树用,非库字段) */
+	private String treeLeaf;
 	private Integer sort;
 	private String title;
 	private String tid;
@@ -224,6 +226,17 @@ public class MesProductRecord extends DataEntity<MesProductRecord> {
 		this.parentCode = parentCode;
 	}
 
+	/**
+	 * 树表叶子标识:1=叶子明细,0=聚合节点(非库字段)
+	 */
+	public String getTreeLeaf() {
+		return treeLeaf;
+	}
+
+	public void setTreeLeaf(String treeLeaf) {
+		this.treeLeaf = treeLeaf;
+	}
+
 	public Integer getSort() {
 		return sort;
 	}

+ 52 - 0
src/main/java/com/jeesite/modules/mes/service/MesLineProcessConsumeService.java

@@ -0,0 +1,52 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesLineProcessConsumeDao;
+import com.jeesite.modules.mes.entity.MesLineProcessConsume;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 工位加工消耗物料绑定 Service
+ * @author mes
+ * @version 2026-07-14
+ */
+@Service
+public class MesLineProcessConsumeService extends CrudService<MesLineProcessConsumeDao, MesLineProcessConsume> {
+
+	@Override
+	public MesLineProcessConsume get(MesLineProcessConsume mesLineProcessConsume) {
+		return super.get(mesLineProcessConsume);
+	}
+
+	@Override
+	public Page<MesLineProcessConsume> findPage(MesLineProcessConsume mesLineProcessConsume) {
+		return super.findPage(mesLineProcessConsume);
+	}
+
+	@Override
+	public List<MesLineProcessConsume> findList(MesLineProcessConsume mesLineProcessConsume) {
+		return super.findList(mesLineProcessConsume);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesLineProcessConsume mesLineProcessConsume) {
+		super.save(mesLineProcessConsume);
+	}
+
+	@Override
+	@Transactional
+	public void updateStatus(MesLineProcessConsume mesLineProcessConsume) {
+		super.updateStatus(mesLineProcessConsume);
+	}
+
+	@Override
+	@Transactional
+	public void delete(MesLineProcessConsume mesLineProcessConsume) {
+		super.delete(mesLineProcessConsume);
+	}
+}

+ 53 - 3
src/main/java/com/jeesite/modules/mes/service/MesMaterialKcLogService.java

@@ -2,8 +2,8 @@ package com.jeesite.modules.mes.service;
 
 import java.util.List;
 
-import org.checkerframework.checker.units.qual.A;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -25,6 +25,10 @@ public class MesMaterialKcLogService extends CrudService<MesMaterialKcLogDao, Me
 	@Autowired
 	private MesMaterialKcLogDao mesMaterialKcLogDao;
 
+	@Autowired
+	@Lazy
+	private MesMaterialKcTotalService mesMaterialKcTotalService;
+
 	/**
 	 * 获取单条数据
 	 * @param mesMaterialKcLog
@@ -95,13 +99,31 @@ public class MesMaterialKcLogService extends CrudService<MesMaterialKcLogDao, Me
 	}
 	
 	/**
-	 * 保存数据(插入或更新)
+	 * 保存数据(插入或更新),并按数量变化同步物料总库存
 	 * @param mesMaterialKcLog
 	 */
 	@Override
 	@Transactional
 	public void save(MesMaterialKcLog mesMaterialKcLog) {
+		long oldNums = 0L;
+		String oldMaterialId = null;
+		boolean isNew = mesMaterialKcLog == null || mesMaterialKcLog.getIsNewRecord();
+		if (!isNew && mesMaterialKcLog != null && StringUtils.isNotBlank(mesMaterialKcLog.getId())) {
+			MesMaterialKcLog old = super.get(mesMaterialKcLog.getId());
+			if (old != null) {
+				oldNums = old.getNums() == null ? 0L : old.getNums();
+				oldMaterialId = old.getMaterialId();
+			} else {
+				isNew = true;
+			}
+		}
 		super.save(mesMaterialKcLog);
+		if (mesMaterialKcLog == null) {
+			return;
+		}
+		long newNums = mesMaterialKcLog.getNums() == null ? 0L : mesMaterialKcLog.getNums();
+		String newMaterialId = mesMaterialKcLog.getMaterialId();
+		syncKcTotal(oldMaterialId, oldNums, newMaterialId, newNums, isNew);
 	}
 	
 	/**
@@ -115,13 +137,41 @@ public class MesMaterialKcLogService extends CrudService<MesMaterialKcLogDao, Me
 	}
 	
 	/**
-	 * 删除数据
+	 * 删除数据,并扣减物料总库存
 	 * @param mesMaterialKcLog
 	 */
 	@Override
 	@Transactional
 	public void delete(MesMaterialKcLog mesMaterialKcLog) {
+		MesMaterialKcLog old = null;
+		if (mesMaterialKcLog != null && StringUtils.isNotBlank(mesMaterialKcLog.getId())) {
+			old = super.get(mesMaterialKcLog.getId());
+		}
 		super.delete(mesMaterialKcLog);
+		if (old != null) {
+			long oldNums = old.getNums() == null ? 0L : old.getNums();
+			mesMaterialKcTotalService.adjustBy(old.getMaterialId(), -oldNums);
+		}
+	}
+
+	/**
+	 * 同步物料总库存:新增按新数量增加;更新按差值调整;物料变更时先减旧再加新。
+	 */
+	private void syncKcTotal(String oldMaterialId, long oldNums, String newMaterialId, long newNums, boolean isNew) {
+		if (isNew) {
+			mesMaterialKcTotalService.adjustBy(newMaterialId, newNums);
+			return;
+		}
+		boolean materialChanged = StringUtils.isNotBlank(oldMaterialId)
+				&& StringUtils.isNotBlank(newMaterialId)
+				&& !oldMaterialId.equals(newMaterialId);
+		if (materialChanged) {
+			mesMaterialKcTotalService.adjustBy(oldMaterialId, -oldNums);
+			mesMaterialKcTotalService.adjustBy(newMaterialId, newNums);
+			return;
+		}
+		String materialId = StringUtils.isNotBlank(newMaterialId) ? newMaterialId : oldMaterialId;
+		mesMaterialKcTotalService.adjustBy(materialId, newNums - oldNums);
 	}
 	
 }

+ 153 - 0
src/main/java/com/jeesite/modules/mes/service/MesMaterialKcTotalService.java

@@ -0,0 +1,153 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesMaterialKcTotalDao;
+import com.jeesite.modules.mes.entity.MesMaterialKcTotal;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 物料总库存 Service(按 material_id 汇总)
+ * @author mes
+ * @version 2026-07-14
+ */
+@Service
+public class MesMaterialKcTotalService extends CrudService<MesMaterialKcTotalDao, MesMaterialKcTotal> {
+
+	private final MesMaterialKcTotalDao mesMaterialKcTotalDao;
+	private final JdbcTemplate jdbcTemplate;
+
+	public MesMaterialKcTotalService(MesMaterialKcTotalDao mesMaterialKcTotalDao, JdbcTemplate jdbcTemplate) {
+		this.mesMaterialKcTotalDao = mesMaterialKcTotalDao;
+		this.jdbcTemplate = jdbcTemplate;
+	}
+
+	@Override
+	public MesMaterialKcTotal get(MesMaterialKcTotal mesMaterialKcTotal) {
+		return super.get(mesMaterialKcTotal);
+	}
+
+	@Override
+	public Page<MesMaterialKcTotal> findPage(MesMaterialKcTotal mesMaterialKcTotal) {
+		return super.findPage(mesMaterialKcTotal);
+	}
+
+	@Override
+	public List<MesMaterialKcTotal> findList(MesMaterialKcTotal mesMaterialKcTotal) {
+		return super.findList(mesMaterialKcTotal);
+	}
+
+	/**
+	 * 按物料ID查询总库存记录
+	 */
+	public MesMaterialKcTotal findByMaterialId(String materialId) {
+		if (StringUtils.isBlank(materialId)) {
+			return null;
+		}
+		return mesMaterialKcTotalDao.findByMaterialId(materialId.trim());
+	}
+
+	/**
+	 * 按物料增减总库存;无记录时自动创建。
+	 */
+	@Transactional
+	public void adjustBy(String materialId, long delta) {
+		if (StringUtils.isBlank(materialId) || delta == 0L) {
+			return;
+		}
+		String mid = materialId.trim();
+		MesMaterialKcTotal total = mesMaterialKcTotalDao.findByMaterialId(mid);
+		if (total == null) {
+			total = new MesMaterialKcTotal();
+			total.setMaterialId(mid);
+			total.setTotalNums(delta);
+			total.setRemarks("库存变更自动创建");
+			super.save(total);
+			return;
+		}
+		long current = total.getTotalNums() == null ? 0L : total.getTotalNums();
+		total.setTotalNums(current + delta);
+		super.save(total);
+	}
+
+	/**
+	 * 按明细重算单个物料总库存
+	 */
+	@Transactional
+	public void recalculateOne(String materialId) {
+		if (StringUtils.isBlank(materialId)) {
+			return;
+		}
+		String mid = materialId.trim();
+		Long sum = mesMaterialKcTotalDao.sumNumsByMaterialId(mid);
+		long totalNums = sum == null ? 0L : sum;
+		MesMaterialKcTotal total = mesMaterialKcTotalDao.findByMaterialId(mid);
+		if (total == null) {
+			if (totalNums == 0L) {
+				return;
+			}
+			total = new MesMaterialKcTotal();
+			total.setMaterialId(mid);
+			total.setTotalNums(totalNums);
+			total.setRemarks("按明细重算");
+			super.save(total);
+			return;
+		}
+		total.setTotalNums(totalNums);
+		total.setRemarks("按明细重算");
+		super.save(total);
+	}
+
+	/**
+	 * 按全部明细重算汇总表(纠偏)
+	 */
+	@Transactional
+	public int recalculateAll() {
+		Date now = new Date();
+		jdbcTemplate.update("DELETE FROM mes_material_kc_total");
+		List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+				"SELECT material_id AS materialId, COALESCE(SUM(nums), 0) AS totalNums "
+						+ "FROM mes_material_kc_log "
+						+ "WHERE material_id IS NOT NULL AND material_id <> '' "
+						+ "GROUP BY material_id");
+		int count = 0;
+		for (Map<String, Object> row : rows) {
+			Object materialIdObj = row.get("materialId");
+			if (materialIdObj == null) {
+				continue;
+			}
+			String materialId = String.valueOf(materialIdObj);
+			long totalNums = toLong(row.get("totalNums"));
+			MesMaterialKcTotal total = new MesMaterialKcTotal();
+			total.setMaterialId(materialId);
+			total.setTotalNums(totalNums);
+			total.setRemarks("全量重算");
+			total.setCreateDate(now);
+			total.setUpdateDate(now);
+			super.save(total);
+			count++;
+		}
+		return count;
+	}
+
+	private long toLong(Object value) {
+		if (value == null) {
+			return 0L;
+		}
+		if (value instanceof Number) {
+			return ((Number) value).longValue();
+		}
+		try {
+			return Long.parseLong(String.valueOf(value));
+		} catch (Exception e) {
+			return 0L;
+		}
+	}
+}

+ 40 - 0
src/main/java/com/jeesite/modules/mes/service/MesProcessConsumeLogService.java

@@ -0,0 +1,40 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesProcessConsumeLogDao;
+import com.jeesite.modules.mes.entity.MesProcessConsumeLog;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 加工消耗扣料日志 Service
+ * @author mes
+ * @version 2026-07-14
+ */
+@Service
+public class MesProcessConsumeLogService extends CrudService<MesProcessConsumeLogDao, MesProcessConsumeLog> {
+
+	@Override
+	public MesProcessConsumeLog get(MesProcessConsumeLog entity) {
+		return super.get(entity);
+	}
+
+	@Override
+	public Page<MesProcessConsumeLog> findPage(MesProcessConsumeLog entity) {
+		return super.findPage(entity);
+	}
+
+	@Override
+	public List<MesProcessConsumeLog> findList(MesProcessConsumeLog entity) {
+		return super.findList(entity);
+	}
+
+	@Override
+	@Transactional
+	public void save(MesProcessConsumeLog entity) {
+		super.save(entity);
+	}
+}

+ 255 - 0
src/main/java/com/jeesite/modules/mes/service/MesProcessConsumeStockService.java

@@ -0,0 +1,255 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.collect.ListUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.modules.mes.entity.MesLineProcess;
+import com.jeesite.modules.mes.entity.MesLineProcessConsume;
+import com.jeesite.modules.mes.entity.MesMaterial;
+import com.jeesite.modules.mes.entity.MesMaterialKcLog;
+import com.jeesite.modules.mes.entity.MesMaterialKcTotal;
+import com.jeesite.modules.mes.entity.MesProcessConsumeLog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.ObjectUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工位加工消耗:库存校验 + OK 后 FIFO 扣减
+ * @author mes
+ * @version 2026-07-14
+ */
+@Service
+public class MesProcessConsumeStockService {
+
+	private static final Logger logger = LoggerFactory.getLogger(MesProcessConsumeStockService.class);
+
+	private final MesLineProcessService mesLineProcessService;
+	private final MesLineProcessConsumeService mesLineProcessConsumeService;
+	private final MesMaterialKcTotalService mesMaterialKcTotalService;
+	private final MesMaterialKcLogService mesMaterialKcLogService;
+	private final MesProcessConsumeLogService mesProcessConsumeLogService;
+	private final JdbcTemplate jdbcTemplate;
+
+	public MesProcessConsumeStockService(MesLineProcessService mesLineProcessService,
+			MesLineProcessConsumeService mesLineProcessConsumeService,
+			MesMaterialKcTotalService mesMaterialKcTotalService,
+			MesMaterialKcLogService mesMaterialKcLogService,
+			MesProcessConsumeLogService mesProcessConsumeLogService,
+			JdbcTemplate jdbcTemplate) {
+		this.mesLineProcessService = mesLineProcessService;
+		this.mesLineProcessConsumeService = mesLineProcessConsumeService;
+		this.mesMaterialKcTotalService = mesMaterialKcTotalService;
+		this.mesMaterialKcLogService = mesMaterialKcLogService;
+		this.mesProcessConsumeLogService = mesProcessConsumeLogService;
+		this.jdbcTemplate = jdbcTemplate;
+	}
+
+	/**
+	 * 库存校验结果
+	 */
+	public static class StockCheckResult {
+		private final boolean ok;
+		private final String message;
+
+		private StockCheckResult(boolean ok, String message) {
+			this.ok = ok;
+			this.message = message;
+		}
+
+		public static StockCheckResult pass() {
+			return new StockCheckResult(true, null);
+		}
+
+		public static StockCheckResult fail(String message) {
+			return new StockCheckResult(false, message);
+		}
+
+		public boolean isOk() {
+			return ok;
+		}
+
+		public String getMessage() {
+			return message;
+		}
+	}
+
+	/**
+	 * pccheck:校验父工位绑定物料库存是否足够(无绑定则直接通过)
+	 */
+	public StockCheckResult checkStockEnough(String oprno, String lineSn) {
+		MesLineProcess process = mesLineProcessService.findInfoByOprnoLineSn(oprno, lineSn);
+		if (ObjectUtils.isEmpty(process) || StringUtils.isBlank(process.getId())) {
+			return StockCheckResult.pass();
+		}
+		String parentProcessId = resolveParentProcessId(process);
+		List<MesLineProcessConsume> binds = listActiveConsume(parentProcessId);
+		if (ListUtils.isEmpty(binds)) {
+			return StockCheckResult.pass();
+		}
+		for (MesLineProcessConsume bind : binds) {
+			int need = bind.getNums() == null ? 0 : bind.getNums();
+			if (need <= 0) {
+				continue;
+			}
+			long available = availableNums(bind.getMaterialId());
+			if (available < need) {
+				String name = StringUtils.defaultIfBlank(bind.getMaterialTitle(), bind.getMaterialId());
+				String erp = StringUtils.defaultIfBlank(bind.getErpSn(), "-");
+				return StockCheckResult.fail("加工物料库存不足:" + name + "(" + erp + ") 需要" + need
+						+ ",当前" + available);
+			}
+		}
+		return StockCheckResult.pass();
+	}
+
+	/**
+	 * pcresult:OK 成功后按父工位绑定 FIFO 扣库存(幂等)
+	 */
+	@Transactional
+	public void consumeOnOk(String sn, String oprno, String lineSn, String userCode) {
+		if (StringUtils.isBlank(sn) || StringUtils.isBlank(oprno) || StringUtils.isBlank(lineSn)) {
+			return;
+		}
+		String useOprno = oprno;
+		if (useOprno.length() == 5) {
+			useOprno = useOprno + "A";
+		}
+		if (alreadyConsumed(sn, useOprno, lineSn)) {
+			logger.info("加工扣料已执行,跳过 sn={}, oprno={}, lineSn={}", sn, useOprno, lineSn);
+			return;
+		}
+		MesLineProcess process = mesLineProcessService.findInfoByOprnoLineSn(useOprno, lineSn);
+		if (ObjectUtils.isEmpty(process) || StringUtils.isBlank(process.getId())) {
+			return;
+		}
+		String parentProcessId = resolveParentProcessId(process);
+		List<MesLineProcessConsume> binds = listActiveConsume(parentProcessId);
+		if (ListUtils.isEmpty(binds)) {
+			return;
+		}
+		StockCheckResult check = checkStockEnough(useOprno, lineSn);
+		if (!check.isOk()) {
+			throw new ServiceException(StringUtils.defaultIfBlank(check.getMessage(), "加工物料库存不足"));
+		}
+		String createBy = StringUtils.defaultIfBlank(userCode, "system");
+		for (MesLineProcessConsume bind : binds) {
+			int need = bind.getNums() == null ? 0 : bind.getNums();
+			if (need <= 0 || StringUtils.isBlank(bind.getMaterialId())) {
+				continue;
+			}
+			deductFifo(sn, useOprno, lineSn, parentProcessId, bind.getMaterialId(), need, createBy);
+		}
+	}
+
+	private boolean alreadyConsumed(String sn, String oprno, String lineSn) {
+		MesProcessConsumeLog query = new MesProcessConsumeLog();
+		query.setSn(sn);
+		query.setOprno(oprno);
+		query.setLineSn(lineSn);
+		Long count = mesProcessConsumeLogService.findCount(query);
+		return count != null && count > 0;
+	}
+
+	/**
+	 * 父工位 ID:有 pid 用父级,否则自身
+	 */
+	public String resolveParentProcessId(MesLineProcess process) {
+		if (process == null) {
+			return null;
+		}
+		if (StringUtils.isNotBlank(process.getPid())) {
+			return process.getPid();
+		}
+		return process.getId();
+	}
+
+	private List<MesLineProcessConsume> listActiveConsume(String parentProcessId) {
+		if (StringUtils.isBlank(parentProcessId)) {
+			return Collections.emptyList();
+		}
+		MesLineProcessConsume query = new MesLineProcessConsume();
+		query.setLineProcessId(parentProcessId);
+		query.setStatus(MesMaterial.STATUS_NORMAL);
+		return mesLineProcessConsumeService.findList(query);
+	}
+
+	private long availableNums(String materialId) {
+		if (StringUtils.isBlank(materialId)) {
+			return 0L;
+		}
+		MesMaterialKcTotal total = mesMaterialKcTotalService.findByMaterialId(materialId);
+		if (total == null || total.getTotalNums() == null) {
+			return 0L;
+		}
+		return Math.max(0L, total.getTotalNums());
+	}
+
+	/**
+	 * 按创建时间 FIFO 扣减明细库存,并写消耗日志
+	 */
+	private void deductFifo(String sn, String oprno, String lineSn, String parentProcessId,
+			String materialId, int need, String createBy) {
+		List<MesMaterialKcLog> stocks = findPositiveStockFifo(materialId);
+		int remain = need;
+		for (MesMaterialKcLog stock : stocks) {
+			if (remain <= 0) {
+				break;
+			}
+			long cur = stock.getNums() == null ? 0L : stock.getNums();
+			if (cur <= 0) {
+				continue;
+			}
+			long take = Math.min(cur, remain);
+			stock.setNums(cur - take);
+			mesMaterialKcLogService.save(stock);
+
+			MesProcessConsumeLog log = new MesProcessConsumeLog();
+			log.setSn(sn);
+			log.setOprno(oprno);
+			log.setLineSn(lineSn);
+			log.setParentProcessId(parentProcessId);
+			log.setMaterialId(materialId);
+			log.setNums((int) take);
+			log.setBatchSn(stock.getBatchSn());
+			log.setKcLogId(stock.getId());
+			log.setCreateBy(createBy);
+			log.setRemarks("加工OK扣料");
+			mesProcessConsumeLogService.save(log);
+			remain -= (int) take;
+		}
+		if (remain > 0) {
+			throw new ServiceException("加工物料库存不足,扣减失败 materialId=" + materialId + " 剩余差额=" + remain);
+		}
+	}
+
+	private List<MesMaterialKcLog> findPositiveStockFifo(String materialId) {
+		List<Map<String, Object>> rows = jdbcTemplate.queryForList(
+				"SELECT id FROM mes_material_kc_log "
+						+ "WHERE material_id = ? AND nums IS NOT NULL AND nums > 0 "
+						+ "ORDER BY create_date ASC, id ASC",
+				materialId);
+		if (rows == null || rows.isEmpty()) {
+			return new ArrayList<>();
+		}
+		List<MesMaterialKcLog> list = new ArrayList<>();
+		for (Map<String, Object> row : rows) {
+			Object idObj = row.get("id");
+			if (idObj == null) {
+				continue;
+			}
+			MesMaterialKcLog kc = mesMaterialKcLogService.get(String.valueOf(idObj));
+			if (kc != null) {
+				list.add(kc);
+			}
+		}
+		return list;
+	}
+}

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

@@ -46,6 +46,33 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 	@Autowired
 	private MesProductRecordDao mesProductRecordDao;
 
+	/**
+	 * 列表聚合:按工件码分组分页
+	 * 统计总数时需临时去掉 Page,避免分页拦截器给 count SQL 加 LIMIT,
+	 * 导致第 2 页起 count 无结果而报错(页面提示「您访问的页面出错啦」)。
+	 */
+	public Page<MesProductRecord> findSnGroupPage(MesProductRecord query) {
+		Page<MesProductRecord> page = query.getPage();
+		query.setPage(null);
+		long count = mesProductRecordDao.findSnGroupCount(query);
+		page.setCount(count);
+		query.setPage(page);
+		page.setList(mesProductRecordDao.findSnGroupList(query));
+		return page;
+	}
+
+	/**
+	 * 列表聚合:指定工件下的工位列表
+	 */
+	public List<MesProductRecord> findOprnoGroupList(String sn) {
+		if (StringUtils.isBlank(sn)) {
+			return ListUtils.newArrayList();
+		}
+		MesProductRecord query = new MesProductRecord();
+		query.setSn(sn);
+		return mesProductRecordDao.findOprnoGroupList(query);
+	}
+
 	@Autowired
 	private MesProductCateService mesProductCateService;
 
@@ -401,6 +428,10 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 				mpr1.setTreeLevel(0);
 				mpr1.setCdate(cdate);
 				mpr1.setContent("UDF");
+				if (!mesProductRecord.getCreateBy().isEmpty()){
+					mpr1.setCreateBy(mesProductRecord.getCreateBy());
+					mpr1.setUpdateBy(mesProductRecord.getCreateBy());
+				}
 				if(mesTemplateItems.getOprno().equals(oprno)){
 					mpr1.setOprno(mesProductRecord.getOprno());
 					mpr1.setContent(mesProductRecord.getContent());
@@ -427,6 +458,10 @@ public class MesProductRecordService extends CrudService<MesProductRecordDao, Me
 			mesProduct.setSn(mesProductRecord.getSn());
 			mesProduct.setState("0");
 			mesProduct.setCate(cateSn);
+			if (!mesProductRecord.getCreateBy().isEmpty()){
+				mesProduct.setCreateBy(mesProductRecord.getCreateBy());
+				mesProduct.setUpdateBy(mesProductRecord.getCreateBy());
+			}
 			mesProductService.save(mesProduct);
 
 			return "RSOK";

+ 23 - 0
src/main/java/com/jeesite/modules/mes/web/MesBatchSnController.java

@@ -102,6 +102,29 @@ public class MesBatchSnController extends BaseController {
 		model.addAttribute("mesBatchSn", mesBatchSn);
 		return "modules/mes/mesBatchSnForm";
 	}
+
+	/**
+	 * 批次码二维码打印页(支持单条 id 或多选 ids,逗号分隔)
+	 */
+	@RequiresPermissions("mes:mesBatchSn:view")
+	@RequestMapping(value = "qrcode")
+	public String qrcode(String id, String ids, Model model) {
+		List<MesBatchSn> list = ListUtils.newArrayList();
+		String idStr = StringUtils.isNotBlank(ids) ? ids : id;
+		if (StringUtils.isNotBlank(idStr)) {
+			for (String itemId : StringUtils.split(idStr, ",")) {
+				if (StringUtils.isBlank(itemId)) {
+					continue;
+				}
+				MesBatchSn item = mesBatchSnService.get(itemId.trim());
+				if (item != null && StringUtils.isNotBlank(item.getBatchSn())) {
+					list.add(item);
+				}
+			}
+		}
+		model.addAttribute("mesBatchSnList", list);
+		return "modules/mes/mesBatchSnQrcode";
+	}
 	/**
 	 * 查看编辑表单
 	 */

+ 130 - 0
src/main/java/com/jeesite/modules/mes/web/MesLineProcessConsumeController.java

@@ -0,0 +1,130 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesLineProcess;
+import com.jeesite.modules.mes.entity.MesLineProcessConsume;
+import com.jeesite.modules.mes.entity.MesMaterial;
+import com.jeesite.modules.mes.service.MesLineProcessConsumeService;
+import com.jeesite.modules.mes.service.MesLineProcessService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 工位加工消耗物料绑定 Controller
+ * @author mes
+ * @version 2026-07-14
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesLineProcessConsume")
+public class MesLineProcessConsumeController extends BaseController {
+
+	private final MesLineProcessConsumeService mesLineProcessConsumeService;
+	private final MesLineProcessService mesLineProcessService;
+
+	public MesLineProcessConsumeController(MesLineProcessConsumeService mesLineProcessConsumeService,
+			MesLineProcessService mesLineProcessService) {
+		this.mesLineProcessConsumeService = mesLineProcessConsumeService;
+		this.mesLineProcessService = mesLineProcessService;
+	}
+
+	@ModelAttribute
+	public MesLineProcessConsume get(String id, boolean isNewRecord) {
+		return mesLineProcessConsumeService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:view")
+	@RequestMapping(value = {"list", "bindList", ""})
+	public String list(MesLineProcessConsume mesLineProcessConsume, Model model) {
+		model.addAttribute("mesLineProcessConsume", mesLineProcessConsume);
+		model.addAttribute("mlInfo", loadProcess(mesLineProcessConsume.getLineProcessId()));
+		return "modules/mes/mesLineProcessConsumeList";
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesLineProcessConsume> listData(MesLineProcessConsume mesLineProcessConsume,
+			HttpServletRequest request, HttpServletResponse response) {
+		mesLineProcessConsume.setPage(new Page<>(request, response));
+		return mesLineProcessConsumeService.findPage(mesLineProcessConsume);
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:view")
+	@RequestMapping(value = {"form", "bindForm"})
+	public String form(MesLineProcessConsume mesLineProcessConsume, Model model) {
+		model.addAttribute("mesLineProcessConsume", mesLineProcessConsume);
+		String lineProcessId = mesLineProcessConsume.getLineProcessId();
+		if (StringUtils.isBlank(lineProcessId) && StringUtils.isNotBlank(mesLineProcessConsume.getId())) {
+			MesLineProcessConsume db = mesLineProcessConsumeService.get(mesLineProcessConsume);
+			if (db != null) {
+				lineProcessId = db.getLineProcessId();
+			}
+		}
+		model.addAttribute("mlInfo", loadProcess(lineProcessId));
+		return "modules/mes/mesLineProcessConsumeForm";
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:edit")
+	@PostMapping(value = "save")
+	@ResponseBody
+	public String save(@Validated MesLineProcessConsume mesLineProcessConsume) {
+		if (mesLineProcessConsume.getNums() == null || mesLineProcessConsume.getNums() <= 0) {
+			return renderResult(Global.FALSE, text("每次加工消耗数量必须大于 0!"));
+		}
+		if (mesLineProcessConsume.getIsNewRecord()) {
+			MesLineProcessConsume query = new MesLineProcessConsume();
+			query.setLineProcessId(mesLineProcessConsume.getLineProcessId());
+			query.setMaterialId(mesLineProcessConsume.getMaterialId());
+			if (mesLineProcessConsumeService.findCount(query) > 0) {
+				return renderResult(Global.FALSE, text("当前工位已绑定该物料!"));
+			}
+		}
+		mesLineProcessConsumeService.save(mesLineProcessConsume);
+		return renderResult(Global.TRUE, text("保存工位加工物料绑定成功!"));
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:edit")
+	@RequestMapping(value = "disable")
+	@ResponseBody
+	public String disable(MesLineProcessConsume mesLineProcessConsume) {
+		mesLineProcessConsume.setStatus(MesMaterial.STATUS_DISABLE);
+		mesLineProcessConsumeService.updateStatus(mesLineProcessConsume);
+		return renderResult(Global.TRUE, text("停用成功"));
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:edit")
+	@RequestMapping(value = "enable")
+	@ResponseBody
+	public String enable(MesLineProcessConsume mesLineProcessConsume) {
+		mesLineProcessConsume.setStatus(MesMaterial.STATUS_NORMAL);
+		mesLineProcessConsumeService.updateStatus(mesLineProcessConsume);
+		return renderResult(Global.TRUE, text("启用成功"));
+	}
+
+	@RequiresPermissions("mes:mesLineProcessConsume:edit")
+	@RequestMapping(value = "delete")
+	@ResponseBody
+	public String delete(MesLineProcessConsume mesLineProcessConsume) {
+		mesLineProcessConsumeService.delete(mesLineProcessConsume);
+		return renderResult(Global.TRUE, text("删除成功"));
+	}
+
+	private MesLineProcess loadProcess(String lineProcessId) {
+		if (StringUtils.isBlank(lineProcessId)) {
+			return null;
+		}
+		return mesLineProcessService.get(new MesLineProcess(lineProcessId));
+	}
+}

+ 3 - 7
src/main/java/com/jeesite/modules/mes/web/MesLineProcessMaterialController.java

@@ -1,6 +1,5 @@
 package com.jeesite.modules.mes.web;
 
-import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -10,9 +9,6 @@ import com.jeesite.modules.mes.entity.*;
 import com.jeesite.modules.mes.resp.BindMaterialResp;
 import com.jeesite.modules.mes.resp.CommonResp;
 import com.jeesite.modules.mes.service.MesLineProcessService;
-import com.jeesite.modules.mes.service.MesMaterialPrebindService;
-import com.jeesite.modules.sys.entity.User;
-import com.jeesite.modules.sys.utils.UserUtils;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
@@ -29,7 +25,7 @@ import com.jeesite.common.web.BaseController;
 import com.jeesite.modules.mes.service.MesLineProcessMaterialService;
 
 /**
- * 生产工序物料表Controller
+ * 生产工序物料表Controller(旧版;页面入口已切换到 mesLineProcessConsume)
  * @author mes
  * @version 2024-04-02
  */
@@ -52,7 +48,7 @@ public class MesLineProcessMaterialController extends BaseController {
 	}
 	
 	/**
-	 * 查询列表
+	 * 查询列表(旧版页面,工位管理「物料」已不再跳转这里)
 	 */
 	@RequiresPermissions("mes:mesLineProcessMaterial:view")
 	@RequestMapping(value = {"list", ""})
@@ -166,4 +162,4 @@ public class MesLineProcessMaterialController extends BaseController {
 		return resp;
 	}
 	
-}
+}

+ 81 - 0
src/main/java/com/jeesite/modules/mes/web/MesMaterialKcTotalController.java

@@ -0,0 +1,81 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesMaterialKcTotal;
+import com.jeesite.modules.mes.service.MesMaterialKcTotalService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 物料总库存 Controller(只读列表 + 按明细重算)
+ * @author mes
+ * @version 2026-07-14
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesMaterialKcTotal")
+public class MesMaterialKcTotalController extends BaseController {
+
+	private final MesMaterialKcTotalService mesMaterialKcTotalService;
+
+	public MesMaterialKcTotalController(MesMaterialKcTotalService mesMaterialKcTotalService) {
+		this.mesMaterialKcTotalService = mesMaterialKcTotalService;
+	}
+
+	@ModelAttribute
+	public MesMaterialKcTotal get(String id, boolean isNewRecord) {
+		return mesMaterialKcTotalService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesMaterialKcTotal:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesMaterialKcTotal mesMaterialKcTotal, Model model) {
+		model.addAttribute("mesMaterialKcTotal", mesMaterialKcTotal);
+		return "modules/mes/mesMaterialKcTotalList";
+	}
+
+	@RequiresPermissions("mes:mesMaterialKcTotal:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesMaterialKcTotal> listData(MesMaterialKcTotal mesMaterialKcTotal,
+			HttpServletRequest request, HttpServletResponse response) {
+		mesMaterialKcTotal.setPage(new Page<>(request, response));
+		return mesMaterialKcTotalService.findPage(mesMaterialKcTotal);
+	}
+
+	/**
+	 * 按明细库存全量重算汇总
+	 */
+	@RequiresPermissions("mes:mesMaterialKcTotal:edit")
+	@PostMapping(value = "recalculate")
+	@ResponseBody
+	public String recalculate() {
+		int count = mesMaterialKcTotalService.recalculateAll();
+		return renderResult(Global.TRUE, text("重算完成,共更新 ") + count + text(" 种物料库存"));
+	}
+
+	/**
+	 * 按物料查询当前总库存(工位绑物料表单用)
+	 */
+	@RequiresPermissions("mes:mesLineProcessConsume:view")
+	@RequestMapping(value = "stockByMaterial")
+	@ResponseBody
+	public String stockByMaterial(String materialId) {
+		if (StringUtils.isBlank(materialId)) {
+			return renderResult(Global.TRUE, text("查询成功"), 0L);
+		}
+		MesMaterialKcTotal total = mesMaterialKcTotalService.findByMaterialId(materialId);
+		long nums = (total == null || total.getTotalNums() == null) ? 0L : total.getTotalNums();
+		return renderResult(Global.TRUE, text("查询成功"), nums);
+	}
+}

+ 57 - 0
src/main/java/com/jeesite/modules/mes/web/MesProcessConsumeLogController.java

@@ -0,0 +1,57 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProcessConsumeLog;
+import com.jeesite.modules.mes.req.MesSearchDateReq;
+import com.jeesite.modules.mes.service.MesProcessConsumeLogService;
+import com.jeesite.modules.mes.util.CommonUitl;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 加工消耗扣料日志(只读查询)
+ * @author mes
+ * @version 2026-07-14
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesProcessConsumeLog")
+public class MesProcessConsumeLogController extends BaseController {
+
+	private final MesProcessConsumeLogService mesProcessConsumeLogService;
+
+	public MesProcessConsumeLogController(MesProcessConsumeLogService mesProcessConsumeLogService) {
+		this.mesProcessConsumeLogService = mesProcessConsumeLogService;
+	}
+
+	@ModelAttribute
+	public MesProcessConsumeLog get(String id, boolean isNewRecord) {
+		return mesProcessConsumeLogService.get(id, isNewRecord);
+	}
+
+	@RequiresPermissions("mes:mesProcessConsumeLog:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(MesProcessConsumeLog mesProcessConsumeLog, Model model) {
+		MesSearchDateReq dateMap = CommonUitl.getSearchDate();
+		mesProcessConsumeLog.setCreateDate_gte(dateMap.getStartDate());
+		mesProcessConsumeLog.setCreateDate_lte(dateMap.getEndtDate());
+		model.addAttribute("mesProcessConsumeLog", mesProcessConsumeLog);
+		return "modules/mes/mesProcessConsumeLogList";
+	}
+
+	@RequiresPermissions("mes:mesProcessConsumeLog:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<MesProcessConsumeLog> listData(MesProcessConsumeLog mesProcessConsumeLog,
+			HttpServletRequest request, HttpServletResponse response) {
+		mesProcessConsumeLog.setPage(new Page<>(request, response));
+		return mesProcessConsumeLogService.findPage(mesProcessConsumeLog);
+	}
+}

+ 13 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductArcWeldingController.java

@@ -85,6 +85,7 @@ public class MesProductArcWeldingController extends BaseController {
 	@RequiresPermissions("mes:mesProductArcWelding:view")
 	@RequestMapping(value = "listSpot")
 	public String listSpot(MesProductArcWelding mesProductArcWelding, Model model) {
+		applyDefaultCreateDate(mesProductArcWelding);
 		mesProductArcWelding.setWeldingCategory("spot");
 		model.addAttribute("mesProductArcWelding", mesProductArcWelding);
 		return "modules/mes/mesProductSpotWeldingList";
@@ -96,10 +97,22 @@ public class MesProductArcWeldingController extends BaseController {
 	@RequiresPermissions("mes:mesProductArcWelding:view")
 	@RequestMapping(value = "listArc")
 	public String listArc(MesProductArcWelding mesProductArcWelding, Model model) {
+		applyDefaultCreateDate(mesProductArcWelding);
 		mesProductArcWelding.setWeldingCategory("arc");
 		model.addAttribute("mesProductArcWelding", mesProductArcWelding);
 		return "modules/mes/mesProductArcWeldingList";
 	}
+
+	/** 默认带上创建时间查询范围 */
+	private void applyDefaultCreateDate(MesProductArcWelding mesProductArcWelding) {
+		MesSearchDateReq dateMap = CommonUitl.getSearchDate();
+		if (mesProductArcWelding.getCreateDate_gte() == null) {
+			mesProductArcWelding.setCreateDate_gte(dateMap.getStartDate());
+		}
+		if (mesProductArcWelding.getCreateDate_lte() == null) {
+			mesProductArcWelding.setCreateDate_lte(dateMap.getEndtDate());
+		}
+	}
 	
 	/**
 	 * 查询列表数据

+ 124 - 17
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -180,6 +180,9 @@ public class MesProductRecordController extends BaseController {
 	@Autowired
 	 private MesProductProgressService mesProductProgressService;
 
+	@Autowired
+	private MesProcessConsumeStockService mesProcessConsumeStockService;
+
 
 	/**
 	 * 获取数据
@@ -299,29 +302,111 @@ public class MesProductRecordController extends BaseController {
 		return renderResult(Global.TRUE, text("操作成功!"));
 	}
 	/**
-	 * 查询列表数据
+	 * 查询列表数据(按工件码聚合分页)
 	 */
 	@RequiresPermissions("mes:mesProductRecord:view")
 	@RequestMapping(value = "listData")
 	@ResponseBody
-	public Page<MesProductRecord> listData(MesProductRecord mesProductRecord, HttpServletRequest request, HttpServletResponse response) {
+	public Page<MesProductRecord> listData(MesProductRecord mesProductRecord,
+			HttpServletRequest request, HttpServletResponse response) {
+		applyProcessStateFilter(mesProductRecord);
+		Page<MesProductRecord> page = new Page<>(request, response);
+		mesProductRecord.setPage(page);
+		// 按 sn 聚合,不按明细主键排序
+		mesProductRecord.getSqlMap().getOrder().setOrderBy("");
+		page = mesProductRecordService.findSnGroupPage(mesProductRecord);
+		if (!ListUtils.isEmpty(page.getList())) {
+			for (MesProductRecord row : page.getList()) {
+				if (row.getSerial() != null) {
+					row.setRemark("明细 " + row.getSerial() + " 条");
+				}
+			}
+		}
+		return page;
+	}
 
-		String sn = mesProductRecord.getSn();
+	/**
+	 * 工件明细弹层页面(工位 → 工艺)
+	 */
+	@RequiresPermissions("mes:mesProductRecord:view")
+	@RequestMapping(value = "detailList")
+	public String detailList(String sn, Model model) {
+		if (StringUtils.isBlank(sn)) {
+			sn = "";
+		}
+		model.addAttribute("sn", sn);
+		return "modules/mes/mesProductRecordDetailList";
+	}
 
-		Page page1 = new Page<>(request, response);
-		mesProductRecord.setPage(page1);
-		mesProductRecord.getSqlMap().getOrder().setOrderBy("a.id DESC");
-		// 如果查询状态的值为 1, 表示用户选择查询 已加工
-		if("1".equals(mesProductRecord.getProcessState()) && "".equals(mesProductRecord.getContent()))
-			mesProductRecord.getSqlMap().getWhere().and("content",QueryType.NE, "UDF");
-		// 如果查询状态的值为 0, 表示用户选择查询 未加工
-		else if ("0".equals(mesProductRecord.getProcessState()) && "".equals(mesProductRecord.getContent()))
-			mesProductRecord.getSqlMap().getWhere().and("content",QueryType.EQ, "UDF");
-		Page<MesProductRecord> page = mesProductRecordService.findPage(mesProductRecord);
-		fillProductRecordListFields(page.getList());
+	/**
+	 * 明细弹层:指定工件下的工位列表
+	 */
+	@RequiresPermissions("mes:mesProductRecord:view")
+	@RequestMapping(value = "listOprnoData")
+	@ResponseBody
+	public Page<MesProductRecord> listOprnoData(String sn, HttpServletRequest request, HttpServletResponse response) {
+		Page<MesProductRecord> page = new Page<>(request, response);
+		List<MesProductRecord> list = mesProductRecordService.findOprnoGroupList(sn);
+		if (!ListUtils.isEmpty(list)) {
+			Map<String, String> oprnoTitleMap = new HashMap<>();
+			for (MesProductRecord row : list) {
+				if (StringUtils.isBlank(row.getLineSn()) || StringUtils.isBlank(row.getOprno())) {
+					row.setOprnoTitle("");
+					continue;
+				}
+				String oprno = CommonUitl.formatOprno(row.getOprno());
+				String key = row.getLineSn() + "|" + oprno;
+				if (!oprnoTitleMap.containsKey(key)) {
+					MesLineProcess mesLineProcess = new MesLineProcess();
+					mesLineProcess.setLineSn(row.getLineSn());
+					mesLineProcess.setOprno(oprno);
+					MesLineProcess info = mesLineProcessService.findInfo(mesLineProcess);
+					oprnoTitleMap.put(key, ObjectUtils.isEmpty(info) ? "" : info.getTitle());
+				}
+				row.setOprnoTitle(oprnoTitleMap.get(key));
+			}
+		}
+		page.setList(list);
+		page.setCount(list == null ? 0 : list.size());
+		return page;
+	}
+
+	/**
+	 * 明细弹层:指定工件+工位的工艺明细
+	 */
+	@RequiresPermissions("mes:mesProductRecord:view")
+	@RequestMapping(value = "listDetailData")
+	@ResponseBody
+	public Page<MesProductRecord> listDetailData(String sn, String oprno,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<MesProductRecord> page = new Page<>(request, response);
+		if (StringUtils.isBlank(sn)) {
+			page.setList(ListUtils.newArrayList());
+			return page;
+		}
+		MesProductRecord query = new MesProductRecord();
+		query.setSn(sn);
+		if (StringUtils.isNotBlank(oprno)) {
+			query.getSqlMap().getWhere().and("oprno", QueryType.EQ, oprno);
+		}
+		query.getSqlMap().getOrder().setOrderBy("a.oprno ASC, a.serial ASC, a.id ASC");
+		List<MesProductRecord> list = mesProductRecordService.findList(query);
+		fillProductRecordListFields(list);
+		page.setList(list);
+		page.setCount(list == null ? 0 : list.size());
 		return page;
 	}
 
+	private void applyProcessStateFilter(MesProductRecord mesProductRecord) {
+		if ("1".equals(mesProductRecord.getProcessState())
+				&& (mesProductRecord.getContent() == null || "".equals(mesProductRecord.getContent()))) {
+			mesProductRecord.getSqlMap().getWhere().and("content", QueryType.NE, "UDF");
+		} else if ("0".equals(mesProductRecord.getProcessState())
+				&& (mesProductRecord.getContent() == null || "".equals(mesProductRecord.getContent()))) {
+			mesProductRecord.getSqlMap().getWhere().and("content", QueryType.EQ, "UDF");
+		}
+	}
+
 	private void fillProductRecordListFields(List<MesProductRecord> list) {
 		if (ListUtils.isEmpty(list)) {
 			return;
@@ -1417,6 +1502,11 @@ public class MesProductRecordController extends BaseController {
 		paramsResp2.setResult("OK");
 		params.add(paramsResp2);
 
+		ParamsResp paramsResp3 = new ParamsResp();
+		paramsResp3.setCraft("400001");
+		paramsResp3.setResult(user);
+		params.add(paramsResp3);
+
 		try{
 
 			// 2.判断物料批次是否符合绑定
@@ -1747,9 +1837,9 @@ public class MesProductRecordController extends BaseController {
 		}
 
 		String passoprnos = "";
-		if(lineSn.equals("XT")){
-			passoprnos = Global.getConfig("mes.pass.oprno.xt");
-		}
+
+		passoprnos = Global.getConfig("mes.pass.oprno.xt");
+
 
 		// 万能码逻辑
 		String universalSn = Global.getConfig("mes.universal.sn");
@@ -4217,6 +4307,15 @@ public class MesProductRecordController extends BaseController {
 		String ret = checkQualityCommon(oldOprno,sn,lineSn,userCode);
 		String checkret = ret.substring(3,5);
 		if(checkret.equals("UD")){
+			// 质量通过后:按父工位加工消耗绑定校验库存
+			MesProcessConsumeStockService.StockCheckResult stockCheck =
+					mesProcessConsumeStockService.checkStockEnough(oldOprno, lineSn);
+			if (!stockCheck.isOk()) {
+				resp.setMessage(StringUtils.defaultIfBlank(stockCheck.getMessage(), ErrorMsg.getErrorMsg("KC", "")));
+				resp.setResult(Global.FALSE);
+				resp.setData("KC");
+				return resp;
+			}
 			resp.setMessage("工件可以加工");
 			resp.setResult(Global.TRUE);
 		}else{
@@ -4288,12 +4387,20 @@ public class MesProductRecordController extends BaseController {
 					TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
 					return renderResult(Global.FALSE, text("操作失败!"));
 				}else{
+					// craft=100000 且质量 OK 成功后,按父工位绑定 FIFO 扣库存
+					if ("OK".equals(result)) {
+						mesProcessConsumeStockService.consumeOnOk(sn, oprno, lineSn, userCode);
+					}
 					return renderResult(Global.TRUE, text("操作成功!"));
 				}
 			}
 		} catch (Exception e) {
 			TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
 			logger.info(e.getMessage());
+			String msg = e.getMessage();
+			if (StringUtils.isNotBlank(msg) && msg.contains("库存不足")) {
+				return renderResult(Global.FALSE, text(msg));
+			}
 			return renderResult(Global.FALSE, text("操作失败!"));
 		}
 	}

+ 3 - 3
src/main/java/com/jeesite/modules/mes/web/MesScrapController.java

@@ -131,7 +131,7 @@ public class MesScrapController extends BaseController {
 		}
 	}
 
-	@RequiresPermissions("mes:mesScrap:api")
+//	@RequiresPermissions("mes:mesScrap:api")
 	@RequestMapping(value = "add")
 	@ResponseBody
 	@Transactional
@@ -177,7 +177,7 @@ public class MesScrapController extends BaseController {
 		return resp;
 	}
 
-	@RequiresPermissions("mes:mesScrap:api")
+//	@RequiresPermissions("mes:mesScrap:api")
 	@RequestMapping(value = "ulist")
 	@ResponseBody
 	public CommonResp ulist(MesScrap mesScrap, HttpServletRequest request, HttpServletResponse response) {
@@ -197,7 +197,7 @@ public class MesScrapController extends BaseController {
 	/**
 	 * 详情
 	 */
-	@RequiresPermissions("mes:mesScrap:api")
+//	@RequiresPermissions("mes:mesScrap:api")
 	@RequestMapping(value = "info")
 	@ResponseBody
 	public CommonResp info(MesScrap mesScrap, HttpServletRequest request) {

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

@@ -80,6 +80,8 @@ public class ErrorMsg {
                 lmsg = "子工位未配置!";
             } else if (processMsgRet.equalsIgnoreCase("RK")) {
                 lmsg = "半成品未出库";
+            } else if (processMsgRet.equalsIgnoreCase("KC")) {
+                lmsg = "加工物料库存不足";
             }
         }catch (Exception e){ }
         return lmsg;

+ 3 - 0
src/main/resources/config/application.yml

@@ -847,6 +847,9 @@ shiro:
     ${adminPath}/mes/mesProductProgress/pdaOut= anon
     ${adminPath}/mes/mesMaterialKw/kwList= anon
     ${adminPath}/mes/mesJbLog/getBatchInfo= anon
+    ${adminPath}/mes/mesScrap/add= anon
+    ${adminPath}/mes/mesScrap/ulist= anon
+    ${adminPath}/mes/mesScrap/info= anon
     ${adminPath}/** = user
 
 

+ 4 - 0
src/main/resources/mappings/modules/mes/MesLineProcessConsumeDao.xml

@@ -0,0 +1,4 @@
+<?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.MesLineProcessConsumeDao">
+</mapper>

+ 27 - 0
src/main/resources/mappings/modules/mes/MesMaterialKcTotalDao.xml

@@ -0,0 +1,27 @@
+<?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.MesMaterialKcTotalDao">
+
+	<select id="findByMaterialId" resultType="com.jeesite.modules.mes.entity.MesMaterialKcTotal">
+		SELECT
+			a.id AS "id",
+			a.material_id AS "materialId",
+			a.total_nums AS "totalNums",
+			a.status AS "status",
+			a.create_by AS "createBy",
+			a.create_date AS "createDate",
+			a.update_by AS "updateBy",
+			a.update_date AS "updateDate",
+			a.remarks AS "remarks"
+		FROM mes_material_kc_total a
+		WHERE a.material_id = #{materialId}
+		LIMIT 1
+	</select>
+
+	<select id="sumNumsByMaterialId" resultType="java.lang.Long">
+		SELECT COALESCE(SUM(a.nums), 0)
+		FROM mes_material_kc_log a
+		WHERE a.material_id = #{materialId}
+	</select>
+
+</mapper>

+ 4 - 0
src/main/resources/mappings/modules/mes/MesProcessConsumeLogDao.xml

@@ -0,0 +1,4 @@
+<?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.MesProcessConsumeLogDao">
+</mapper>

+ 50 - 0
src/main/resources/mappings/modules/mes/MesProductRecordDao.xml

@@ -49,4 +49,54 @@
         update mes_product_record set content =#{content}
         where sn = #{sn} and oprno = #{oprno} and line_sn = #{lineSn} and craft = #{craft}
     </update>
+
+	<!-- 列表聚合:按工件码分组(虚拟 id 用 SN_ 前缀;勿用 #/: ,jqGrid 会改写行 id) -->
+	<select id="findSnGroupList" resultType="com.jeesite.modules.mes.entity.MesProductRecord">
+		SELECT
+			CONCAT('SN_', a.sn) AS "id",
+			a.sn AS "sn",
+			MAX(a.cate_sn) AS "cateSn",
+			MAX(a.line_sn) AS "lineSn",
+			MAX(a.update_date) AS "updateDate",
+			MAX(a.update_by) AS "updateBy",
+			COUNT(1) AS "serial",
+			'0' AS "parentCode",
+			'0' AS "treeLeaf"
+		FROM mes_product_record a
+		<where>
+			${sqlMap.where.toSql()}
+		</where>
+		GROUP BY a.sn
+		ORDER BY MAX(a.update_date) DESC, a.sn DESC
+	</select>
+
+	<select id="findSnGroupCount" resultType="long">
+		SELECT COUNT(1) FROM (
+			SELECT a.sn
+			FROM mes_product_record a
+			<where>
+				${sqlMap.where.toSql()}
+			</where>
+			GROUP BY a.sn
+		) t
+	</select>
+
+	<!-- 列表聚合:指定工件下的工位(虚拟 id:OP_sn__oprno;content 取工位总结果 100000) -->
+	<select id="findOprnoGroupList" resultType="com.jeesite.modules.mes.entity.MesProductRecord">
+		SELECT
+			CONCAT('OP_', a.sn, '__', a.oprno) AS "id",
+			a.sn AS "sn",
+			a.oprno AS "oprno",
+			MAX(a.line_sn) AS "lineSn",
+			MAX(a.cate_sn) AS "cateSn",
+			MAX(a.update_date) AS "updateDate",
+			MAX(CASE WHEN a.craft = '100000' THEN a.content ELSE NULL END) AS "content",
+			COUNT(1) AS "serial",
+			CONCAT('SN_', a.sn) AS "parentCode",
+			'0' AS "treeLeaf"
+		FROM mes_product_record a
+		WHERE a.sn = #{sn}
+		GROUP BY a.sn, a.oprno
+		ORDER BY a.oprno ASC
+	</select>
 </mapper>

+ 21 - 2
src/main/resources/views/modules/mes/mesBatchSnList.html

@@ -6,6 +6,7 @@
 				<i class="fa icon-notebook"></i> ${text('批次码记录表管理')}
 			</div>
 			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnPrintBatch" title="${text('批量打印')}"><i class="fa fa-print"></i> ${text('批量打印')}</a>
 				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
 				<a href="${ctx}/mes/mesBatchSn/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>
@@ -57,6 +58,8 @@
 // 初始化DataGrid对象
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
+	showCheckbox: true,
+	multiboxonly: false,
 	columnModel: [
 		{header:'${text("批次码")}', name:'batchSn', index:'a.batch_sn', width:150, align:"center"},
 		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:150, align:"center"},
@@ -68,11 +71,12 @@ $('#dataGrid').dataGrid({
 		{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:150, align:"center"},
 		{header:'${text("更新人")}', name:'updateBy', index:'a.update_by', width:150, align:"center"},
 		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
-		{header:'${text("操作")}', name:'actions', width:160, formatter: function(val, obj, row, act){
+		{header:'${text("操作")}', name:'actions', width:180, formatter: function(val, obj, row, act){
 			var actions = [];
 			if (row.isJs != '2'){
 				actions.push('<a href="${ctx}/mes/mesBatchSn/bindForm?id='+row.id+'" class="btnList" title="${text("绑定工件码")}"><i class="fa fa-link"></i></a>&nbsp;');
 			}
+			actions.push('<a href="${ctx}/mes/mesBatchSn/qrcode?id='+row.id+'" class="btnList" title="${text("打印二维码")}"><i class="fa fa-fw fa-qrcode"></i></a>&nbsp;');
 			actions.push('<a href="${ctx}/mes/mesBatchSn/info?id='+row.id+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
 			return actions.join('');
 		}}
@@ -82,4 +86,19 @@ $('#dataGrid').dataGrid({
 		
 	}
 });
-</script>
+
+// 批量打印:勾选后打开网页二维码打印页
+$('#btnPrintBatch').click(function(){
+	var ids = $('#dataGrid').dataGrid('getSelectRows');
+	if (!ids || ids.length === 0) {
+		js.showMessage('${text("请先勾选要打印的批次码")}');
+		return false;
+	}
+	var url = '${ctx}/mes/mesBatchSn/qrcode?ids=' + encodeURIComponent(ids.join(','));
+	var $a = $('<a href="' + url + '" class="btnList" title="${text("批量打印批次码")}"></a>');
+	$('body').append($a);
+	$a.trigger('click');
+	$a.remove();
+	return false;
+});
+</script>

+ 69 - 0
src/main/resources/views/modules/mes/mesBatchSnQrcode.html

@@ -0,0 +1,69 @@
+<% layout('/layouts/default.html', {title: '批次码二维码', libs: ['qrcode','jqprint']}){ %>
+<style>
+	.qrcode-item { text-align: center; margin: 20px auto; padding: 10px; }
+	.qrcode-item .qrcode-box { width: 200px; height: 200px; margin: 0 auto; }
+	.qrcode-item .qrcode-text { margin-top: 8px; font-size: 16px; font-weight: bold; letter-spacing: 1px; }
+	@media print {
+		.box-header, .box-footer, .main-header, .main-sidebar, .content-header { display: none !important; }
+		.qrcode-item { page-break-after: always; margin: 0; padding: 0; }
+		.qrcode-item:last-child { page-break-after: auto; }
+	}
+</style>
+<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('批次码二维码')}
+			</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>
+		<div class="box-body">
+			<div id="printer">
+				<% for(item in mesBatchSnList!){ %>
+				<div class="qrcode-item" data-batch-sn="${item.batchSn}">
+					<div class="qrcode-box"></div>
+					<div class="qrcode-text">${item.batchSn}</div>
+				</div>
+				<% } %>
+				<% if(isEmpty(mesBatchSnList)){ %>
+				<div class="text-center text-muted" style="padding:40px;">${text('未找到可打印的批次码')}</div>
+				<% } %>
+			</div>
+		</div>
+		<div class="box-footer">
+			<div class="row">
+				<div class="col-sm-12 text-center">
+					<% if(!isEmpty(mesBatchSnList)){ %>
+					<button type="button" class="btn btn-sm btn-primary" onclick="printer()"><i class="fa fa-print"></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>
+	</div>
+</div>
+<% } %>
+<script>
+	$(function () {
+		$('#printer .qrcode-item').each(function () {
+			var batchSn = $(this).attr('data-batch-sn');
+			var box = $(this).find('.qrcode-box')[0];
+			if (batchSn && box) {
+				new QRCode(box, {
+					text: batchSn,
+					width: 200,
+					height: 200,
+					colorDark: "#000000",
+					colorLight: "#ffffff",
+					correctLevel: QRCode.CorrectLevel.H
+				});
+			}
+		});
+	});
+
+	function printer() {
+		$("#printer").print();
+	}
+</script>

+ 108 - 0
src/main/resources/views/modules/mes/mesLineProcessConsumeForm.html

@@ -0,0 +1,108 @@
+<% 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(mesLineProcessConsume.isNewRecord ? '绑定加工物料' : '编辑加工物料绑定')}
+				<% if(isNotBlank(mlInfo!)){ %>
+					<small class="text-muted">([${mlInfo.oprno}] ${mlInfo.title})</small>
+				<% } %>
+			</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>
+		<#form:form id="inputForm" model="${mesLineProcessConsume}" action="${ctx}/mes/mesLineProcessConsume/save" method="post" class="form-horizontal">
+			<div class="box-body">
+				<div class="form-unit">${text('绑定信息')}</div>
+				<#form:hidden path="id"/>
+				<#form:hidden path="lineProcessId"/>
+				<div class="row">
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required">*</span> ${text('物料')}:</label>
+							<div class="col-sm-8">
+								<#form:listselect id="materialId" title="选择物料" name="materialId" value="${mesLineProcessConsume.materialId}"
+								labelName="title" labelValue="${mesLineProcessConsume.materialTitle}" url="${ctx}/mes/mesMaterial/preventSelect"
+								class="required" btnClass="btn-sm" allowClear="true" readonly="false"
+								itemCode="id" itemName="title"/>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								${text('当前总库存')}:</label>
+							<div class="col-sm-8">
+								<input type="text" id="totalNumsView" class="form-control" readonly="readonly"
+									value="${mesLineProcessConsume.totalNums != null ? mesLineProcessConsume.totalNums : '0'}"/>
+								<span class="help-block text-muted">${text('来自物料总库存汇总,保存绑定不会改动库存')}</span>
+							</div>
+						</div>
+					</div>
+					<div class="col-xs-6">
+						<div class="form-group">
+							<label class="control-label col-sm-4" title="">
+								<span class="required">*</span> ${text('每次加工消耗数量')}:</label>
+							<div class="col-sm-8">
+								<#form:input path="nums" maxlength="8" class="form-control digits required"/>
+								<span class="help-block text-muted">${text('单件/每次加工从库存扣减的数量;无需绑定工艺号')}</span>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<% if (hasPermi('mes:mesLineProcessConsume: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>
+function loadTotalNums(materialId) {
+	if (!materialId) {
+		$('#totalNumsView').val('0');
+		return;
+	}
+	js.ajaxSubmit('${ctx}/mes/mesMaterialKcTotal/stockByMaterial', {materialId: materialId}, function(data){
+		if (data && data.result == Global.TRUE) {
+			$('#totalNumsView').val(data.data != null ? data.data : 0);
+		} else {
+			$('#totalNumsView').val('0');
+		}
+	}, 'json', true);
+}
+
+$(document).on('change', '#materialId', function(){
+	loadTotalNums($(this).val());
+});
+$(function(){
+	var mid = $('#materialId').val();
+	if (mid) {
+		loadTotalNums(mid);
+	}
+});
+
+$("#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>

+ 90 - 0
src/main/resources/views/modules/mes/mesLineProcessConsumeList.html

@@ -0,0 +1,90 @@
+<% 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 fa-cubes"></i>
+				<% if(isNotBlank(mlInfo!)){ %>
+					[${mlInfo.oprno}] ${mlInfo.title} -
+				<% } %>
+				${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>
+				<% if(hasPermi('mes:mesLineProcessConsume:edit')){ %>
+					<a href="${ctx}/mes/mesLineProcessConsume/form?lineProcessId=${mesLineProcessConsume.lineProcessId}" 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">
+			<div class="alert alert-info" style="margin-bottom:10px;">
+				<i class="fa fa-info-circle"></i>
+				${text('直接绑定物料与每次加工消耗数量,无需工艺号模板。当前总库存来自「物料总库存」。')}
+			</div>
+			<#form:form id="searchForm" model="${mesLineProcessConsume}" action="${ctx}/mes/mesLineProcessConsume/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<#form:hidden path="lineProcessId"/>
+				<div class="form-group">
+					<label class="control-label">${text('物料名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="materialTitle" maxlength="255" class="form-control width-140"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('ERP料号')}:</label>
+					<div class="control-inline">
+						<#form:input path="erpSn" maxlength="64" class="form-control width-140"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('状态')}:</label>
+					<div class="control-inline width-120">
+						<#form:select path="status" dictType="sys_search_status" blankOption="true" class="form-control isQuick"/>
+					</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>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("物料名称")}', name:'materialTitle', index:'c.title', width:180, align:"center"},
+		{header:'${text("ERP料号")}', name:'erpSn', index:'c.erp_sn', width:140, align:"center"},
+		{header:'${text("每次加工消耗数量")}', name:'nums', index:'a.nums', width:130, align:"center"},
+		{header:'${text("当前总库存")}', name:'totalNums', index:'t.total_nums', width:110, align:"center", formatter: function(val, obj, row, act){
+			return (val == null || val === '') ? '0' : val;
+		}},
+		{header:'${text("追溯类型")}', name:'type', index:'c.type', width:100, align:"center", formatter: function(val, obj, row, act){
+			return js.getDictLabel(${@DictUtils.getDictListJson('mes_material_type')}, val, '${text("未知")}', true);
+		}},
+		{header:'${text("状态")}', name:'status', index:'a.status', width:90, align:"center", formatter: function(val, obj, row, act){
+			return js.getDictLabel(${@DictUtils.getDictListJson('sys_search_status')}, val, '${text("未知")}', true);
+		}},
+		{header:'${text("操作")}', name:'actions', width:140, formatter: function(val, obj, row, act){
+			var actions = [];
+			//<% if(hasPermi('mes:mesLineProcessConsume:edit')){ %>
+				actions.push('<a href="${ctx}/mes/mesLineProcessConsume/form?id='+row.id+'" class="btnList" title="${text("编辑")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+				if (row.status == Global.STATUS_NORMAL){
+					actions.push('<a href="${ctx}/mes/mesLineProcessConsume/disable?id='+row.id+'" class="btnList" title="${text("停用")}" data-confirm="${text("确认要停用该绑定吗?")}"><i class="glyphicon glyphicon-ban-circle"></i></a>&nbsp;');
+				} else if (row.status == Global.STATUS_DISABLE){
+					actions.push('<a href="${ctx}/mes/mesLineProcessConsume/enable?id='+row.id+'" class="btnList" title="${text("启用")}" data-confirm="${text("确认要启用该绑定吗?")}"><i class="glyphicon glyphicon-ok-circle"></i></a>&nbsp;');
+				}
+				actions.push('<a href="${ctx}/mes/mesLineProcessConsume/delete?id='+row.id+'" class="btnList" title="${text("删除")}" data-confirm="${text("确认要删除该绑定吗?")}"><i class="fa fa-trash-o"></i></a>&nbsp;');
+			//<% } %>
+			return actions.join('');
+		}}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>

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

@@ -109,7 +109,10 @@ $('#dataGrid').dataGrid({
 				actions.push('<a href="${ctx}/mes/mesLineProcess/form?id='+row.id+'" class="btn btn-default btn-xs btnList" title="${text("编辑")}">编辑</a>&nbsp;');
 			//<% } %>
 				actions.push('<a href="${ctx}/mes/mesProcessCheck/list?lineProcessId='+row.id+'" class="btn btn-info btn-xs btnList" title="点检项">点检项</a>&nbsp;');
-				actions.push('<a href="${ctx}/mes/mesLineProcessMaterial/list?lineProcessId='+row.id+'" class="btn btn-warning btn-xs btnList" title="物料">物料</a>&nbsp;');
+				// 老版:mesLineProcessMaterial(mes_line_process_material)
+				// actions.push('<a href="${ctx}/mes/mesLineProcessMaterial/list?lineProcessId='+row.id+'" class="btn btn-warning btn-xs btnList" title="物料">物料</a>&nbsp;');
+				// 新版:工位加工消耗物料绑定(mes_line_process_consume)
+				actions.push('<a href="${ctx}/mes/mesLineProcessConsume/list?lineProcessId='+row.id+'" class="btn btn-warning btn-xs btnList" title="物料">物料</a>&nbsp;');
 				actions.push('<a href="${ctx}/mes/mesLineProcessUser/list?lineProcessId='+row.id+'" class="btn btn-success btn-xs btnList" title="人员">人员</a>&nbsp;');
 				actions.push('<a href="${ctx}/mes/mesLineProcess/sublist?pid='+row.id+'" class="btn btn-primary btn-xs btnList" title="子工位">子工位</a>&nbsp;');
 				actions.push('<a href="${ctx}/mes/mesLineProcess/jarManagement?id='+row.id+'" class="btn btn-xs btnList" title="JAR管理" style="background-color: #605ca8; border-color: #605ca8; color: #fff;">JAR管理</a>&nbsp;');

+ 70 - 0
src/main/resources/views/modules/mes/mesMaterialKcTotalList.html

@@ -0,0 +1,70 @@
+<% 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 fa-cubes"></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>
+				<% if(hasPermi('mes:mesMaterialKcTotal:edit')){ %>
+					<a href="#" class="btn btn-default" id="btnRecalculate" title="${text('按明细重算')}"><i class="fa fa-refresh"></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">
+			<div class="alert alert-info" style="margin-bottom:10px;">
+				<i class="fa fa-info-circle"></i>
+				${text('按物料列表种类汇总当前总库存;明细变动时自动同步。数量异常时可点「按明细重算」。')}
+			</div>
+			<#form:form id="searchForm" model="${mesMaterialKcTotal}" action="${ctx}/mes/mesMaterialKcTotal/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="materialTitle" maxlength="255" class="form-control width-140"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('ERP料号')}:</label>
+					<div class="control-inline">
+						<#form:input path="erpSn" maxlength="64" class="form-control width-140"/>
+					</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>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("物料名称")}', name:'materialTitle', index:'m.title', width:200, align:"center"},
+		{header:'${text("ERP料号")}', name:'erpSn', index:'m.erp_sn', width:160, align:"center"},
+		{header:'${text("规格")}', name:'spec', index:'m.spec', width:160, align:"center"},
+		{header:'${text("总库存")}', name:'totalNums', index:'a.total_nums', width:100, align:"center"},
+		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+
+$('#btnRecalculate').click(function(){
+	js.confirm('${text("确认按来料库存明细全量重算总库存吗?")}', function(){
+		js.ajaxSubmit('${ctx}/mes/mesMaterialKcTotal/recalculate', {}, function(data){
+			js.showMessage(data.message);
+			if (data.result == Global.TRUE) {
+				$('#dataGrid').dataGrid('reloadGrid');
+			}
+		});
+	});
+});
+</script>

+ 11 - 10
src/main/resources/views/modules/mes/mesMaterialSelect.html

@@ -67,19 +67,20 @@ var selectData = ${isNotBlank(selectData!) ? selectData! : "{\}"},
 selectNum = 0, dataGrid = $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
-		{header:'${text("部件名称")}', name:'title', index:'a.title', width:150, align:"left", frozen:true, formatter: function(val, obj, row, act){
+		{header:'${text("部件名称")}', name:'title', index:'a.title', width:200, align:"left", frozen:true, formatter: function(val, obj, row, act){
 				return (val||row.id);
 			}},
 		{header:'${text("部件规格")}', name:'spec', index:'a.spec', width:150, align:"left"},
-		{header:'${text("ERP编码")}', name:'erpSn', index:'a.erp_sn', width:150, align:"left"},
-		{header:'${text("内部图号")}', name:'imgSn', index:'a.img_sn', width:150, align:"left"},
-		{header:'${text("数量/套")}', name:'nums', index:'a.nums', width:150, align:"center"},
-		{header:'${text("加工属性")}', name:'attr', index:'a.attr', width:150, align:"left"},
-		{header:'${text("供应商")}', name:'supplier', index:'a.supplier', width:150, align:"left"},
-		{header:'${text("有效期")}', name:'aging', index:'a.aging', width:150, align:"center"},
-		{header:'${text("追溯类型")}', name:'type', index:'a.type', width:150, align:"center", formatter: function(val, obj, row, act){
-				return js.getDictLabel(${@DictUtils.getDictListJson('mes_material_type')}, val, '${text("未知")}', true);
-			}},
+		{header:'${text("ERP编码")}', name:'erpSn', index:'a.erp_sn', width:180, align:"left"},
+		// 选择弹窗隐藏:内部图号、数量/套、加工属性、供应商、追溯类型
+		// {header:'${text("内部图号")}', name:'imgSn', index:'a.img_sn', width:150, align:"left"},
+		// {header:'${text("数量/套")}', name:'nums', index:'a.nums', width:150, align:"center"},
+		// {header:'${text("加工属性")}', name:'attr', index:'a.attr', width:150, align:"left"},
+		// {header:'${text("供应商")}', name:'supplier', index:'a.supplier', width:150, align:"left"},
+		{header:'${text("有效期")}', name:'aging', index:'a.aging', width:100, align:"center"},
+		// {header:'${text("追溯类型")}', name:'type', index:'a.type', width:150, align:"center", formatter: function(val, obj, row, act){
+		// 		return js.getDictLabel(${@DictUtils.getDictListJson('mes_material_type')}, val, '${text("未知")}', true);
+		// 	}},
 		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
 		{header:'行数据', name:'rowData', hidden:true, formatter: function(val, obj, row, act){
 			return JSON.stringify(row);

+ 102 - 0
src/main/resources/views/modules/mes/mesProcessConsumeLogList.html

@@ -0,0 +1,102 @@
+<% 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 fa-list-alt"></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="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<div class="alert alert-info" style="margin-bottom:10px;">
+				<i class="fa fa-info-circle"></i>
+				${text('加工结果 OK 后自动扣料产生的记录,仅供查询;同一工件+工位+产线不会重复扣减。')}
+			</div>
+			<#form:form id="searchForm" model="${mesProcessConsumeLog}" action="${ctx}/mes/mesProcessConsumeLog/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="createDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="createDate_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="createDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工件码')}:</label>
+					<div class="control-inline">
+						<#form:input path="sn" maxlength="64" class="form-control width-140"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('工位号')}:</label>
+					<div class="control-inline">
+						<#form:input path="oprno" maxlength="32" class="form-control width-100"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('产线')}:</label>
+					<div class="control-inline">
+						<#form:input path="lineSn" maxlength="64" class="form-control width-100"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('物料名称')}:</label>
+					<div class="control-inline">
+						<#form:input path="materialTitle" maxlength="255" class="form-control width-120"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('ERP料号')}:</label>
+					<div class="control-inline">
+						<#form:input path="erpSn" 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="batchSn" maxlength="255" class="form-control width-140"/>
+					</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>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("扣料时间")}', name:'createDate', index:'a.create_date', width:150, align:"center"},
+		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:160, align:"center"},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:100, align:"center"},
+		{header:'${text("产线")}', name:'lineSn', index:'a.line_sn', width:90, align:"center"},
+		{header:'${text("父工位")}', name:'parentOprno', index:'p.oprno', width:100, align:"center", formatter: function(val, obj, row, act){
+			if (val && row.parentProcessTitle) {
+				return val + ' ' + row.parentProcessTitle;
+			}
+			return val || row.parentProcessTitle || '-';
+		}},
+		{header:'${text("物料名称")}', name:'materialTitle', index:'m.title', width:160, align:"center"},
+		{header:'${text("ERP料号")}', name:'erpSn', index:'m.erp_sn', width:140, align:"center"},
+		{header:'${text("扣减数量")}', name:'nums', index:'a.nums', width:90, align:"center"},
+		{header:'${text("批次码")}', name:'batchSn', index:'a.batch_sn', width:160, align:"center", formatter: function(val, obj, row, act){
+			return val || '-';
+		}},
+		{header:'${text("操作人")}', name:'createBy', index:'a.create_by', width:100, align:"center"}
+	],
+	ajaxSuccess: function(data){
+	}
+});
+</script>

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

@@ -35,7 +35,7 @@
 					<div class="col-xs-6">
 						<div class="form-group">
 							<label class="control-label col-sm-4" title="">
-								<span class="required hide">*</span> ${text('二维码')}:<i class="fa icon-question hide"></i></label>
+								<span class="required hide">*</span> ${text('工件码')}:<i class="fa icon-question hide"></i></label>
 							<div class="col-sm-8">
 								<#form:input path="sn" maxlength="50" class="form-control"/>
 							</div>

+ 18 - 7
src/main/resources/views/modules/mes/mesProductArcWeldingList.html

@@ -34,12 +34,22 @@
 					</div>
 				</div>
 				<div class="form-group">
-					<label class="control-label">${text('二维码')}:</label>
+					<label class="control-label">${text('工件码')}:</label>
 					<div class="control-inline">
 						<#form:input path="sn" maxlength="50" class="form-control width-120"/>
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('创建时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="createDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="createDate_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="createDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+					</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>
@@ -54,13 +64,14 @@
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
-		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:150, align:"left"},
-		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:150, align:"left"},
-		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:200, align:"left", sortable:false},
-		{header:'${text("二维码")}', name:'sn', index:'a.sn', width:150, align:"left"},
-		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:120, align:"center"},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:100, align:"center"},
+		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:180, align:"center", sortable:false},
+		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:200, align:"center"},
+		{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:160, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:100, align:"center", formatter: function(val, obj, row, act){
 			var actions = [];
-			actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate)+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
+			actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate||'')+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
 			return actions.join('');
 		}}
 	],

+ 189 - 0
src/main/resources/views/modules/mes/mesProductRecordDetailList.html

@@ -0,0 +1,189 @@
+<% layout('/layouts/default.html', {title: '生产明细', libs: ['dataGrid']}){ %>
+<style>
+.detail-split {
+	display: flex;
+	gap: 10px;
+	align-items: stretch;
+	min-height: calc(100vh - 80px);
+	padding: 10px;
+	box-sizing: border-box;
+}
+.detail-split .detail-pane {
+	display: flex;
+	flex-direction: column;
+	margin: 0;
+	min-width: 0;
+}
+.detail-split .detail-pane-left {
+	flex: 0 0 38%;
+	max-width: 38%;
+}
+.detail-split .detail-pane-right {
+	flex: 1 1 62%;
+}
+.detail-split .box-body {
+	flex: 1;
+	overflow: auto;
+}
+.detail-split .craft-empty {
+	padding: 40px 16px;
+	text-align: center;
+	color: #999;
+}
+.detail-result-ok { color: #28a745; font-weight: bold; }
+.detail-result-udf { color: #f0ad4e; font-weight: bold; }
+.detail-result-ng { color: #dc3545; font-weight: bold; }
+</style>
+<div class="main-content" style="padding:0;">
+	<div class="detail-split">
+		<div class="box box-main detail-pane detail-pane-left">
+			<div class="box-header with-border">
+				<div class="box-title">
+					<i class="fa fa-list-alt"></i> ${text('工位')}:${sn}
+				</div>
+			</div>
+			<div class="box-body">
+				<form id="oprnoSearchForm" action="${ctx}/mes/mesProductRecord/listOprnoData" method="post" class="hide">
+					<input type="hidden" name="sn" value="${sn}"/>
+				</form>
+				<table id="oprnoGrid"></table>
+			</div>
+		</div>
+		<div class="box box-main detail-pane detail-pane-right" id="craftBox">
+			<div class="box-header with-border">
+				<div class="box-title">
+					<i class="fa fa-sitemap"></i> ${text('工艺明细')}
+					<span id="craftBoxTitle" class="text-muted" style="font-size:14px;margin-left:8px;"></span>
+				</div>
+			</div>
+			<div class="box-body">
+				<div id="craftEmpty" class="craft-empty">${text('请点击左侧「查看工艺」加载明细')}</div>
+				<form id="craftSearchForm" action="${ctx}/mes/mesProductRecord/listDetailData" method="post" class="hide">
+					<input type="hidden" name="sn" value="${sn}"/>
+					<input type="hidden" name="oprno" id="craftOprno" value=""/>
+				</form>
+				<table id="craftGrid" style="display:none;"></table>
+			</div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+var detailSn = '${sn}';
+var craftGridInited = false;
+
+/** 结果着色:OK 绿 / UDF 黄 / NG 红 */
+function formatResultColor(val) {
+	if (val == null || val === '') {
+		return '';
+	}
+	var text = String(val);
+	var upper = text.toUpperCase();
+	var cls = '';
+	if (upper === 'OK') {
+		cls = 'detail-result-ok';
+	} else if (upper === 'UDF') {
+		cls = 'detail-result-udf';
+	} else if (upper === 'NG') {
+		cls = 'detail-result-ng';
+	}
+	if (cls) {
+		return '<span class="'+cls+'">'+text+'</span>';
+	}
+	return text;
+}
+
+/** 按当前面板宽度同步表格宽度 */
+function resizeDetailGrids() {
+	setTimeout(function(){
+		var $left = $('.detail-pane-left .box-body');
+		var $right = $('.detail-pane-right .box-body');
+		if ($('#oprnoGrid').length && $left.length) {
+			try { $('#oprnoGrid').jqGrid('setGridWidth', $left.width() - 2); } catch(e) {}
+		}
+		if (craftGridInited && $('#craftGrid').is(':visible') && $right.length) {
+			try { $('#craftGrid').jqGrid('setGridWidth', $right.width() - 2); } catch(e) {}
+		}
+	}, 50);
+}
+
+function openCraftList(oprno, oprnoTitle) {
+	$('#craftEmpty').hide();
+	$('#craftGrid').show();
+	var title = (oprno || '') + (oprnoTitle ? (' ' + oprnoTitle) : '');
+	$('#craftBoxTitle').text(title ? ('- ' + title) : '');
+	$('#craftOprno').val(oprno || '');
+	if (!craftGridInited) {
+		$('#craftGrid').dataGrid({
+			searchForm: $('#craftSearchForm'),
+			columnModel: [
+				{header:'${text("工艺号")}', name:'craft', index:'a.craft', width:80, align:"center"},
+				{header:'${text("报文名称")}', name:'title', index:'b.title', width:150, align:"center", sortable:false},
+				{header:'${text("结果")}', name:'content', index:'a.content', width:70, align:"center", formatter: function(val, obj, row, act){
+					return formatResultColor(val);
+				}},
+				{header:'${text("绑定内容")}', name:'materielSn', index:'a.materiel_sn', width:140, align:"center"},
+				{header:'${text("创建人")}', name:'createBy', index:'a.create_by', width:80, align:"center"},
+				{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:130, align:"center"},
+				{header:'${text("更新人")}', name:'updateBy', index:'a.update_by', width:80, align:"center"},
+				{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:130, align:"center"},
+				{header:'${text("操作")}', name:'actions', width:110, align:"center", formatter: function(val, obj, row, act){
+					var actions = [];
+					//<% if(hasPermi('mes:mesProductRecord:edit')){ %>
+						actions.push('<a href="${ctx}/mes/mesProductRecord/edit?id='+row.id+'" class="btnList" title="${text("编辑")}"><i class="fa fa-pencil"></i></a>&nbsp;');
+					//<% } %>
+						actions.push('<a href="${ctx}/mes/mesProductRecord/info?id='+row.id+'" class="btnList" title="${text("查看生产记录")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
+						//<% if(hasPermi('mes:mesProductRecord:reload')){ %>
+						actions.push('<a href="${ctx}/mes/mesProductRecord/cz?id='+row.id+'" class="btnList" title="重置" data-confirm="确认要重置该数据吗?"><i class="fa fa-refresh"></i></a>&nbsp;');
+						actions.push('<a href="${ctx}/mes/mesProductRecord/ok?id='+row.id+'" class="btnList" title="OK" data-confirm="确认要该记录OK吗?"><i class="fa fa-check-circle-o"></i></a>&nbsp;');
+						//<% } %>
+					return actions.join('');
+				}}
+			],
+			ajaxSuccess: function(data){
+				resizeDetailGrids();
+			}
+		});
+		craftGridInited = true;
+	} else {
+		$('#craftSearchForm').submit();
+	}
+	resizeDetailGrids();
+}
+
+$('#oprnoGrid').dataGrid({
+	searchForm: $('#oprnoSearchForm'),
+	columnModel: [
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:90, align:"center"},
+		{header:'${text("工位名称")}', name:'oprnoTitle', width:120, align:"center", sortable:false},
+		{header:'${text("结果")}', name:'content', index:'a.content', width:70, align:"center", sortable:false, formatter: function(val, obj, row, act){
+			return formatResultColor(val);
+		}},
+		{header:'${text("明细条数")}', name:'serial', width:70, align:"center"},
+		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:130, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:90, align:"center", formatter: function(val, obj, row, act){
+			return '<a href="javascript:void(0);" class="btn btn-primary btn-xs btn-open-craft" data-oprno="'+(row.oprno||'')+'" data-title="'+(row.oprnoTitle||'')+'">${text("查看工艺")}</a>';
+		}}
+	],
+	ajaxSuccess: function(data){
+		resizeDetailGrids();
+		var list = [];
+		if ($.isArray(data)) {
+			list = data;
+		} else if (data && $.isArray(data.list)) {
+			list = data.list;
+		}
+		// 仅一个工位时直接加载工艺明细
+		if (list.length === 1) {
+			openCraftList(list[0].oprno, list[0].oprnoTitle);
+		}
+	}
+});
+
+$(document).on('click', '.btn-open-craft', function(e){
+	e.preventDefault();
+	openCraftList($(this).attr('data-oprno'), $(this).attr('data-title'));
+});
+
+$(window).on('resize', resizeDetailGrids);
+</script>

+ 55 - 80
src/main/resources/views/modules/mes/mesProductRecordList.html

@@ -18,9 +18,12 @@
 			</div>
 		</div>
 		<div class="box-body">
+			<div class="alert alert-info" style="margin-bottom:10px;">
+				<i class="fa fa-info-circle"></i>
+				${text('列表按工件码聚合。点击「查看明细」弹出模态层,可先看工位再看工艺明细。')}
+			</div>
 			<#form:form id="searchForm" model="${mesProductRecord}" action="${ctx}/mes/mesProductRecord/listData" method="post" class="form-inline hide"
 					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
-				<#form:hidden path="pid"/>
 				<div class="form-group">
 					<label class="control-label">${text('工件码')}:</label>
 					<div class="control-inline">
@@ -39,18 +42,6 @@
 						<#form:input path="craft" maxlength="30" class="form-control width-120"/>
 					</div>
 				</div>
-<!--				<div class="form-group">-->
-<!--					<label class="control-label">${text('工位结果序号')}:</label>-->
-<!--					<div class="control-inline">-->
-<!--						<#form:input path="serial" maxlength="8" class="form-control width-120"/>-->
-<!--					</div>-->
-<!--				</div>-->
-<!--				<div class="form-group">-->
-<!--					<label class="control-label">${text('报文类型')}:</label>-->
-<!--					<div class="control-inline">-->
-<!--						<#form:input path="messageType" maxlength="50" class="form-control width-120"/>-->
-<!--					</div>-->
-<!--				</div>-->
 				<div class="form-group">
 					<label class="control-label">${text('工件类型')}:</label>
 					<div class="control-inline">
@@ -69,33 +60,22 @@
 						<#form:input path="content" maxlength="1000" class="form-control width-120"/>
 					</div>
 				</div>
-<!--				<div class="form-group">-->
-<!--					<label class="control-label">${text('备注')}:</label>-->
-<!--					<div class="control-inline">-->
-<!--						<#form:input path="remark" maxlength="255" class="form-control width-120"/>-->
-<!--					</div>-->
-<!--				</div>-->
-
-
-						<div class="form-group">
-							<label class="control-label">${text('加工状态')}:</label>
-							<div class="control-inline" style="width: 80px">
-								<#form:select path="processState" dictType="mes_product_record_process_state" blankOption="true" class="form-control width-120" />
-							</div>
-						</div>
-
-
-
-			<div class="form-group">
-				<label class="control-label">${text('更新日期')}:</label>
-				<div class="control-inline">
-					<#form:input path="updateDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
-					dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="updateDate_lte.click()"/>
-					&nbsp;-&nbsp;
-					<#form:input path="updateDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
-					dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+				<div class="form-group">
+					<label class="control-label">${text('加工状态')}:</label>
+					<div class="control-inline" style="width: 80px">
+						<#form:select path="processState" dictType="mes_product_record_process_state" blankOption="true" class="form-control width-120" />
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('更新日期')}:</label>
+					<div class="control-inline">
+						<#form:input path="updateDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+						dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="updateDate_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="updateDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+						dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+					</div>
 				</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>
@@ -108,59 +88,54 @@
 </div>
 <% } %>
 <script>
-// 初始化DataGrid对象
+/** 打开工件明细模态层(工位 → 工艺) */
+function openProductRecordDetail(sn) {
+	if (!sn) {
+		return;
+	}
+	js.layer.open({
+		type: 2,
+		title: '${text("生产明细")} - ' + sn,
+		area: ['92%', '88%'],
+		maxmin: true,
+		shadeClose: true,
+		scrollbar: true,
+		content: '${ctx}/mes/mesProductRecord/detailList?sn=' + encodeURIComponent(sn)
+	});
+}
+
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
-		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:280, align:"center", frozen:true,  formatter: function(val, obj, row, act){
-			return '<a href="${ctx}/mes/mesProductRecord/info?id='+row.id+'" class="btnList" data-title="${text("查看生产记录")}">'+(val||row.id)+'</a>';
+		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:260, align:"center", formatter: function(val, obj, row, act){
+			var sn = val || '';
+			if (!sn) {
+				return '';
+			}
+			return '<a href="javascript:void(0);" class="btn-open-detail" data-sn="'+sn+'">'+sn+'</a>';
 		}},
-		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:90, align:"center"},
-		{header:'${text("工位名称")}', name:'oprnoTitle', index:'oprnoTitle', width:150, align:"center", sortable:false},
-		{header:'${text("工艺号")}', name:'craft', index:'a.craft', width:90, align:"center"},
-		{header:'${text("报文名称")}', name:'title', index:'b.title', width:180, align:"center",sortable:false},
-		{header:'${text("结果")}', name:'content', index:'a.content', width:80, align:"center"},
-		{header:'${text("绑定内容")}', name:'materielSn', index:'a.materiel_sn', width:180, align:"center"},
-		{header:'${text("工件类型")}', name:'cateSn', index:'a.cate_sn', width:90, align:"center"},
-		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:90, align:"center"},
-		// {header:'${text("工位结果序号")}', name:'sort', index:'b.sort', width:120, align:"center",sortable:true},
-		// {header:'${text("报文类型")}', name:'messageType', index:'b.cate', width:100, align:"center"},
+		{header:'${text("工件类型")}', name:'cateSn', index:'a.cate_sn', width:100, align:"center"},
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:100, align:"center"},
 		{header:'${text("备注")}', name:'remark', index:'a.remark', width:150, align:"center"},
-		{header:'${text("来源")}', name:'source', index:'a.source', width:150, align:"center", formatter: function(val, obj, row, act){
-				return js.getDictLabel(${@DictUtils.getDictListJson('mes_product_modify_source')}, val, '${text("未知")}', true);
-			}},
-		{header:'${text("创建人")}', name:'createBy', index:'a.create_by', width:120, align:"center"},
-		{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:120, align:"center"},
 		{header:'${text("更新人")}', name:'updateBy', index:'a.update_by', width:120, align:"center"},
-		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:125, align:"center"},
-		//{header:'${text("更新日期")}', name:'updateDate', index:'a.update_date', width:160, align:"center",frozen:true },
-		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
-			var actions = [];
-			//<% if(hasPermi('mes:mesProductRecord:edit')){ %>
-				actions.push('<a href="${ctx}/mes/mesProductRecord/edit?id='+row.id+'" class="btnList" title="${text("编辑")}"><i class="fa fa-pencil"></i></a>&nbsp;');
-			//<% } %>
-				actions.push('<a href="${ctx}/mes/mesProductRecord/info?id='+row.id+'" class="btnList" title="${text("查看生产记录")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
-				//<% if(hasPermi('mes:mesProductRecord:reload')){ %>
-				actions.push('<a href="${ctx}/mes/mesProductRecord/cz?id='+row.id+'" class="btnList" title="重置" data-confirm="确认要重置该数据吗?"><i class="fa  fa-refresh"></i></a>&nbsp;');
-				//
-				// if(row.content != "OK"){
-					actions.push('<a href="${ctx}/mes/mesProductRecord/ok?id='+row.id+'" class="btnList" title="OK" data-confirm="确认要该记录OK吗?"><i class="fa fa-check-circle-o"></i></a>&nbsp;');
-				// }
-				//<% } %>
-				return actions.join('');
+		{header:'${text("更新时间")}', name:'updateDate', index:'a.update_date', width:150, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:120, align:"center", formatter: function(val, obj, row, act){
+			var sn = row.sn || '';
+			if (!sn) {
+				return '';
+			}
+			return '<a href="javascript:void(0);" class="btn btn-info btn-xs btn-open-detail" data-sn="'+sn+'">${text("查看明细")}</a>';
 		}}
 	],
-	// treeGrid: true,			// 启用树结构表格
-	// defaultExpandLevel: 0,	// 默认展开的层次
-	// // treeGridModel: 'adjacency',
-	frozenCols: true,
-	// expandNodeClearPostData: 'sn,oprno,craft,serial,messageType,materielSn,remark,updateDate',// 展开节点清理请求参数数据(一般设置查询条件的字段属性,否则在查询后,不能展开子节点数据)
-	// 加载成功后执行事件
 	ajaxSuccess: function(data){
-		
 	}
 });
 
+$(document).on('click', '.btn-open-detail', function(e){
+	e.preventDefault();
+	openProductRecordDetail($(this).attr('data-sn'));
+});
+
 $('#btnExport').click(function(){
 	js.ajaxSubmitForm($('#searchForm'), {
 		url:'${ctx}/mes/mesProductRecord/exportData',

+ 18 - 7
src/main/resources/views/modules/mes/mesProductSpotWeldingList.html

@@ -34,12 +34,22 @@
 					</div>
 				</div>
 				<div class="form-group">
-					<label class="control-label">${text('二维码')}:</label>
+					<label class="control-label">${text('工件码')}:</label>
 					<div class="control-inline">
 						<#form:input path="sn" maxlength="50" class="form-control width-120"/>
 					</div>
 				</div>
 				<div class="form-group">
+					<label class="control-label">${text('创建时间')}:</label>
+					<div class="control-inline">
+						<#form:input path="createDate_gte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm" data-done="createDate_lte.click()"/>
+						&nbsp;-&nbsp;
+						<#form:input path="createDate_lte" readonly="true" maxlength="20" class="form-control laydate width-datetime"
+							dataFormat="datetime" data-type="datetime" data-format="yyyy-MM-dd HH:mm"/>
+					</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>
@@ -54,13 +64,14 @@
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
-		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:150, align:"left"},
-		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:150, align:"left"},
-		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:200, align:"left", sortable:false},
-		{header:'${text("二维码")}', name:'sn', index:'a.sn', width:150, align:"left"},
-		{header:'${text("操作")}', name:'actions', width:120, formatter: function(val, obj, row, act){
+		{header:'${text("产线编号")}', name:'lineSn', index:'a.line_sn', width:120, align:"center"},
+		{header:'${text("工位号")}', name:'oprno', index:'a.oprno', width:100, align:"center"},
+		{header:'${text("工位名称")}', name:'oprnoTitle', index:'b.title', width:180, align:"center", sortable:false},
+		{header:'${text("工件码")}', name:'sn', index:'a.sn', width:200, align:"center"},
+		{header:'${text("创建时间")}', name:'createDate', index:'a.create_date', width:160, align:"center"},
+		{header:'${text("操作")}', name:'actions', width:100, align:"center", formatter: function(val, obj, row, act){
 			var actions = [];
-			actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate)+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
+			actions.push('<a href="${ctx}/mes/mesProductArcWelding/info?id='+row.id+'&createDate='+encodeURIComponent(row.createDate||'')+'" class="btnList" title="${text("详情")}"><i class="fa fa-fw fa-eye"></i></a>&nbsp;');
 			return actions.join('');
 		}}
 	],