Prechádzať zdrojové kódy

ERP对接,生产入库功能

jingbo 4 mesiacov pred
rodič
commit
aba6f0730c
38 zmenil súbory, kde vykonal 11114 pridanie a 9 odobranie
  1. 5906 1
      .gitignore
  2. 11 6
      src/main/java/com/jeesite/modules/mes/dao/MesBatchSnDao.java
  3. 12 0
      src/main/java/com/jeesite/modules/mes/dao/MesErpPrdInstockLogDao.java
  4. 61 0
      src/main/java/com/jeesite/modules/mes/entity/MesBatchSn.java
  5. 134 0
      src/main/java/com/jeesite/modules/mes/entity/MesErpPrdInstockLog.java
  6. 16 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPickMaterialBillService.java
  7. 234 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStock.java
  8. 68 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockResult.java
  9. 360 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockSaveService.java
  10. 547 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockService.java
  11. 61 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockSubmitRequest.java
  12. 56 0
      src/main/java/com/jeesite/modules/mes/erp/ErpProductionOrder.java
  13. 167 0
      src/main/java/com/jeesite/modules/mes/erp/ErpProductionOrderService.java
  14. 280 0
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirect.java
  15. 48 0
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectResult.java
  16. 310 0
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSaveService.java
  17. 331 0
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectService.java
  18. 93 0
      src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSubmitRequest.java
  19. 169 0
      src/main/java/com/jeesite/modules/mes/erp/KingdeeErpProperties.java
  20. 68 0
      src/main/java/com/jeesite/modules/mes/erp/MesErpLineMaterialMapping.java
  21. 58 0
      src/main/java/com/jeesite/modules/mes/service/MesBatchSnService.java
  22. 19 0
      src/main/java/com/jeesite/modules/mes/service/MesErpPrdInstockLogService.java
  23. 426 0
      src/main/java/com/jeesite/modules/mes/web/ErpPrdInStockController.java
  24. 103 1
      src/main/java/com/jeesite/modules/mes/web/ErpProductionOrderController.java
  25. 188 0
      src/main/java/com/jeesite/modules/mes/web/ErpTransferDirectController.java
  26. 23 0
      src/main/resources/config/application.yml
  27. 30 0
      src/main/resources/mappings/modules/mes/MesBatchSnDao.xml
  28. 41 0
      src/main/resources/views/modules/mes/erpPrdInStockDetail.html
  29. 170 0
      src/main/resources/views/modules/mes/erpPrdInStockForm.html
  30. 88 0
      src/main/resources/views/modules/mes/erpPrdInStockList.html
  31. 66 0
      src/main/resources/views/modules/mes/erpPrdInStockSelectDetail.html
  32. 56 0
      src/main/resources/views/modules/mes/erpPrdInStockSelectOrder.html
  33. 452 0
      src/main/resources/views/modules/mes/erpProductionOrderDownstream.html
  34. 24 1
      src/main/resources/views/modules/mes/erpProductionOrderList.html
  35. 42 0
      src/main/resources/views/modules/mes/erpTransferDirectDetail.html
  36. 255 0
      src/main/resources/views/modules/mes/erpTransferDirectForm.html
  37. 84 0
      src/main/resources/views/modules/mes/erpTransferDirectList.html
  38. 57 0
      src/main/resources/views/modules/mes/erpTransferDirectSelectOrder.html

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5906 - 1
.gitignore


+ 11 - 6
src/main/java/com/jeesite/modules/mes/dao/MesBatchSnDao.java

@@ -3,15 +3,20 @@ 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.MesBatchSn;
-import com.jeesite.modules.mes.entity.MesDevice;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
- * 批次码记录表DAO接口
- * @author mes
- * @version 2024-04-12
+ * 批次码记录表 DAO
  */
 @MyBatisDao
 public interface MesBatchSnDao extends CrudDao<MesBatchSn> {
-    MesBatchSn findInfo(MesBatchSn mesBatchSn);
 
-}
+	MesBatchSn findInfo(MesBatchSn mesBatchSn);
+
+	/**
+	 * 查询待 ERP 生产入库的批次码:nums&gt;0 且未入库,按产线过滤。
+	 */
+	List<MesBatchSn> findPendingInstockList(@Param("lineSnList") List<String> lineSnList);
+}

+ 12 - 0
src/main/java/com/jeesite/modules/mes/dao/MesErpPrdInstockLogDao.java

@@ -0,0 +1,12 @@
+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.MesErpPrdInstockLog;
+
+/**
+ * MES 推 ERP 生产入库日志 DAO
+ */
+@MyBatisDao
+public interface MesErpPrdInstockLogDao extends CrudDao<MesErpPrdInstockLog> {
+}

+ 61 - 0
src/main/java/com/jeesite/modules/mes/entity/MesBatchSn.java

@@ -21,6 +21,12 @@ import java.util.Date;
 		@Column(name="nums", attrName="nums", label="已绑定数量", isUpdateForce=true),
 		@Column(name="is_js", attrName="isJs", label="是否用完"),
 		@Column(name="line_sn", attrName="lineSn"),
+		@Column(name="instock_status", attrName="instockStatus", label="ERP入库状态"),
+		@Column(name="mo_bill_no", attrName="moBillNo", label="生产订单号"),
+		@Column(name="mo_entry_id", attrName="moEntryId", label="生产订单分录"),
+		@Column(name="erp_instock_bill_no", attrName="erpInstockBillNo", label="ERP入库单号"),
+		@Column(name="erp_lot_number", attrName="erpLotNumber", label="ERP批号"),
+		@Column(name="erp_instock_date", attrName="erpInstockDate", label="ERP入库时间", isUpdateForce=true),
 		@Column(name="create_by", attrName="createBy", label="创建人", isUpdate=false, isQuery=false),
 		@Column(name="create_date", attrName="createDate", label="创建时间", isUpdate=false, isUpdateForce=true),
 		@Column(name="update_by", attrName="updateBy", label="update_by", isQuery=false),
@@ -34,6 +40,13 @@ public class MesBatchSn extends DataEntity<MesBatchSn> {
 	private Long nums;		// 已绑定数量
 	private String isJs;		// 是否用完
 	private String lineSn;
+	/** ERP入库状态:0待入库 1已入库 */
+	private String instockStatus;
+	private String moBillNo;
+	private String moEntryId;
+	private String erpInstockBillNo;
+	private String erpLotNumber;
+	private Date erpInstockDate;
 
 	private Date createDate;
 	private Date updateDate;
@@ -95,6 +108,54 @@ public class MesBatchSn extends DataEntity<MesBatchSn> {
 		this.lineSn = lineSn;
 	}
 
+	public String getInstockStatus() {
+		return instockStatus;
+	}
+
+	public void setInstockStatus(String instockStatus) {
+		this.instockStatus = instockStatus;
+	}
+
+	public String getMoBillNo() {
+		return moBillNo;
+	}
+
+	public void setMoBillNo(String moBillNo) {
+		this.moBillNo = moBillNo;
+	}
+
+	public String getMoEntryId() {
+		return moEntryId;
+	}
+
+	public void setMoEntryId(String moEntryId) {
+		this.moEntryId = moEntryId;
+	}
+
+	public String getErpInstockBillNo() {
+		return erpInstockBillNo;
+	}
+
+	public void setErpInstockBillNo(String erpInstockBillNo) {
+		this.erpInstockBillNo = erpInstockBillNo;
+	}
+
+	public String getErpLotNumber() {
+		return erpLotNumber;
+	}
+
+	public void setErpLotNumber(String erpLotNumber) {
+		this.erpLotNumber = erpLotNumber;
+	}
+
+	public Date getErpInstockDate() {
+		return erpInstockDate;
+	}
+
+	public void setErpInstockDate(Date erpInstockDate) {
+		this.erpInstockDate = erpInstockDate;
+	}
+
 	@JsonFormat(
 			pattern = "yyyy-MM-dd HH:mm:ss"
 	)

+ 134 - 0
src/main/java/com/jeesite/modules/mes/entity/MesErpPrdInstockLog.java

@@ -0,0 +1,134 @@
+package com.jeesite.modules.mes.entity;
+
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * MES 推 ERP 生产入库日志
+ */
+@Table(name = "mes_erp_prd_instock_log", alias = "a", label = "ERP生产入库日志", columns = {
+		@Column(name = "id", attrName = "id", label = "主键", isPK = true),
+		@Column(name = "mo_bill_no", attrName = "moBillNo", label = "生产订单号"),
+		@Column(name = "mo_entry_id", attrName = "moEntryId", label = "生产订单分录"),
+		@Column(name = "material_number", attrName = "materialNumber", label = "物料编码"),
+		@Column(name = "erp_bill_no", attrName = "erpBillNo", label = "ERP入库单号"),
+		@Column(name = "erp_lot_number", attrName = "erpLotNumber", label = "ERP批号"),
+		@Column(name = "total_qty", attrName = "totalQty", label = "合计数量", isUpdateForce = true),
+		@Column(name = "batch_count", attrName = "batchCount", label = "批次数", isUpdateForce = true),
+		@Column(name = "stock_number", attrName = "stockNumber", label = "仓库编码"),
+		@Column(name = "stock_loc_number", attrName = "stockLocNumber", label = "仓位编码"),
+		@Column(name = "batch_sn_list", attrName = "batchSnList", label = "批次码列表"),
+		@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 = "更新者"),
+		@Column(name = "update_date", attrName = "updateDate", label = "更新时间", isUpdateForce = true),
+		@Column(name = "remarks", attrName = "remarks", label = "备注"),
+}, orderBy = "a.create_date DESC")
+public class MesErpPrdInstockLog extends DataEntity<MesErpPrdInstockLog> {
+
+	private static final long serialVersionUID = 1L;
+
+	private String moBillNo;
+	private String moEntryId;
+	private String materialNumber;
+	private String erpBillNo;
+	private String erpLotNumber;
+	private BigDecimal totalQty;
+	private Integer batchCount;
+	private String stockNumber;
+	private String stockLocNumber;
+	private String batchSnList;
+
+	public MesErpPrdInstockLog() {
+		this(null);
+	}
+
+	public MesErpPrdInstockLog(String id) {
+		super(id);
+	}
+
+	public String getMoBillNo() {
+		return moBillNo;
+	}
+
+	public void setMoBillNo(String moBillNo) {
+		this.moBillNo = moBillNo;
+	}
+
+	public String getMoEntryId() {
+		return moEntryId;
+	}
+
+	public void setMoEntryId(String moEntryId) {
+		this.moEntryId = moEntryId;
+	}
+
+	public String getMaterialNumber() {
+		return materialNumber;
+	}
+
+	public void setMaterialNumber(String materialNumber) {
+		this.materialNumber = materialNumber;
+	}
+
+	public String getErpBillNo() {
+		return erpBillNo;
+	}
+
+	public void setErpBillNo(String erpBillNo) {
+		this.erpBillNo = erpBillNo;
+	}
+
+	public String getErpLotNumber() {
+		return erpLotNumber;
+	}
+
+	public void setErpLotNumber(String erpLotNumber) {
+		this.erpLotNumber = erpLotNumber;
+	}
+
+	public BigDecimal getTotalQty() {
+		return totalQty;
+	}
+
+	public void setTotalQty(BigDecimal totalQty) {
+		this.totalQty = totalQty;
+	}
+
+	public Integer getBatchCount() {
+		return batchCount;
+	}
+
+	public void setBatchCount(Integer batchCount) {
+		this.batchCount = batchCount;
+	}
+
+	public String getStockNumber() {
+		return stockNumber;
+	}
+
+	public void setStockNumber(String stockNumber) {
+		this.stockNumber = stockNumber;
+	}
+
+	public String getStockLocNumber() {
+		return stockLocNumber;
+	}
+
+	public void setStockLocNumber(String stockLocNumber) {
+		this.stockLocNumber = stockLocNumber;
+	}
+
+	public String getBatchSnList() {
+		return batchSnList;
+	}
+
+	public void setBatchSnList(String batchSnList) {
+		this.batchSnList = batchSnList;
+	}
+}

+ 16 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPickMaterialBillService.java

@@ -119,6 +119,22 @@ public class ErpPickMaterialBillService {
 	}
 
 	/**
+	 * 生产订单下查:关联领料明细分录。
+	 */
+	public List<ErpPickMaterialBill> findTraceListByMoBillNo(String moBillNo) {
+		if (StringUtils.isBlank(moBillNo)) {
+			return new ArrayList<>();
+		}
+		ErpPickMaterialBill query = new ErpPickMaterialBill();
+		query.setMoBillNoQuery(moBillNo.trim());
+		String moFilter = buildProductionOrderFilter(query);
+		if (moFilter == null) {
+			return new ArrayList<>();
+		}
+		return findDetailList(query, moFilter, 0, 2000);
+	}
+
+	/**
 	 * 明细页:指定领料单号的全部分录行。
 	 */
 	public List<ErpPickMaterialBill> findDetailList(String billNo) {

+ 234 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStock.java

@@ -0,0 +1,234 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * ERP 生产入库单(PRD_INSTOCK)BillQuery 行数据。
+ */
+public class ErpPrdInStock implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String id;
+	private String fid;
+	private String billNo;
+	private String documentStatus;
+	private String documentStatusName;
+	private String date;
+	private String prdOrgNumber;
+	private String prdOrgName;
+	private String moBillNo;
+	private String moEntryId;
+	private String entryId;
+	private String seq;
+	private String materialNumber;
+	private String materialName;
+	private String unitName;
+	private String unitNumber;
+	private BigDecimal realQty;
+	private String stockNumber;
+	private String stockName;
+	private String lotNumber;
+
+	/** 查询条件:入库单号(模糊) */
+	private String billNoQuery;
+	/** 查询条件:生产订单号(模糊) */
+	private String moBillNoQuery;
+	/** 查询条件:物料编码(限定 HEV / 115 范围内) */
+	private String materialNumberQuery;
+	/** 查询条件:单据状态 */
+	private String documentStatusQuery;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getFid() {
+		return fid;
+	}
+
+	public void setFid(String fid) {
+		this.fid = fid;
+	}
+
+	public String getBillNo() {
+		return billNo;
+	}
+
+	public void setBillNo(String billNo) {
+		this.billNo = billNo;
+	}
+
+	public String getDocumentStatus() {
+		return documentStatus;
+	}
+
+	public void setDocumentStatus(String documentStatus) {
+		this.documentStatus = documentStatus;
+	}
+
+	public String getDocumentStatusName() {
+		return documentStatusName;
+	}
+
+	public void setDocumentStatusName(String documentStatusName) {
+		this.documentStatusName = documentStatusName;
+	}
+
+	public String getDate() {
+		return date;
+	}
+
+	public void setDate(String date) {
+		this.date = date;
+	}
+
+	public String getPrdOrgNumber() {
+		return prdOrgNumber;
+	}
+
+	public void setPrdOrgNumber(String prdOrgNumber) {
+		this.prdOrgNumber = prdOrgNumber;
+	}
+
+	public String getPrdOrgName() {
+		return prdOrgName;
+	}
+
+	public void setPrdOrgName(String prdOrgName) {
+		this.prdOrgName = prdOrgName;
+	}
+
+	public String getMoBillNo() {
+		return moBillNo;
+	}
+
+	public void setMoBillNo(String moBillNo) {
+		this.moBillNo = moBillNo;
+	}
+
+	public String getMoEntryId() {
+		return moEntryId;
+	}
+
+	public void setMoEntryId(String moEntryId) {
+		this.moEntryId = moEntryId;
+	}
+
+	public String getEntryId() {
+		return entryId;
+	}
+
+	public void setEntryId(String entryId) {
+		this.entryId = entryId;
+	}
+
+	public String getSeq() {
+		return seq;
+	}
+
+	public void setSeq(String seq) {
+		this.seq = seq;
+	}
+
+	public String getMaterialNumber() {
+		return materialNumber;
+	}
+
+	public void setMaterialNumber(String materialNumber) {
+		this.materialNumber = materialNumber;
+	}
+
+	public String getMaterialName() {
+		return materialName;
+	}
+
+	public void setMaterialName(String materialName) {
+		this.materialName = materialName;
+	}
+
+	public String getUnitName() {
+		return unitName;
+	}
+
+	public void setUnitName(String unitName) {
+		this.unitName = unitName;
+	}
+
+	public String getUnitNumber() {
+		return unitNumber;
+	}
+
+	public void setUnitNumber(String unitNumber) {
+		this.unitNumber = unitNumber;
+	}
+
+	public BigDecimal getRealQty() {
+		return realQty;
+	}
+
+	public void setRealQty(BigDecimal realQty) {
+		this.realQty = realQty;
+	}
+
+	public String getStockNumber() {
+		return stockNumber;
+	}
+
+	public void setStockNumber(String stockNumber) {
+		this.stockNumber = stockNumber;
+	}
+
+	public String getStockName() {
+		return stockName;
+	}
+
+	public void setStockName(String stockName) {
+		this.stockName = stockName;
+	}
+
+	public String getLotNumber() {
+		return lotNumber;
+	}
+
+	public void setLotNumber(String lotNumber) {
+		this.lotNumber = lotNumber;
+	}
+
+	public String getBillNoQuery() {
+		return billNoQuery;
+	}
+
+	public void setBillNoQuery(String billNoQuery) {
+		this.billNoQuery = billNoQuery;
+	}
+
+	public String getMoBillNoQuery() {
+		return moBillNoQuery;
+	}
+
+	public void setMoBillNoQuery(String moBillNoQuery) {
+		this.moBillNoQuery = moBillNoQuery;
+	}
+
+	public String getMaterialNumberQuery() {
+		return materialNumberQuery;
+	}
+
+	public void setMaterialNumberQuery(String materialNumberQuery) {
+		this.materialNumberQuery = materialNumberQuery;
+	}
+
+	public String getDocumentStatusQuery() {
+		return documentStatusQuery;
+	}
+
+	public void setDocumentStatusQuery(String documentStatusQuery) {
+		this.documentStatusQuery = documentStatusQuery;
+	}
+}

+ 68 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockResult.java

@@ -0,0 +1,68 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * ERP 生产入库单 Save 结果
+ */
+public class ErpPrdInStockResult implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private boolean success;
+	private String billNo;
+	private String billId;
+	private String lotNumber;
+	private BigDecimal totalQty;
+	private int batchCount;
+	private String message;
+
+	public static ErpPrdInStockResult success(String billNo, String billId, String lotNumber,
+			BigDecimal totalQty, int batchCount) {
+		ErpPrdInStockResult result = new ErpPrdInStockResult();
+		result.success = true;
+		result.billNo = billNo;
+		result.billId = billId;
+		result.lotNumber = lotNumber;
+		result.totalQty = totalQty;
+		result.batchCount = batchCount;
+		result.message = "生产入库单保存成功";
+		return result;
+	}
+
+	public static ErpPrdInStockResult fail(String message) {
+		ErpPrdInStockResult result = new ErpPrdInStockResult();
+		result.success = false;
+		result.message = message;
+		return result;
+	}
+
+	public boolean isSuccess() {
+		return success;
+	}
+
+	public String getBillNo() {
+		return billNo;
+	}
+
+	public String getBillId() {
+		return billId;
+	}
+
+	public String getLotNumber() {
+		return lotNumber;
+	}
+
+	public BigDecimal getTotalQty() {
+		return totalQty;
+	}
+
+	public int getBatchCount() {
+		return batchCount;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+}

+ 360 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockSaveService.java

@@ -0,0 +1,360 @@
+package com.jeesite.modules.mes.erp;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesBatchSn;
+import com.jeesite.modules.mes.entity.MesErpPrdInstockLog;
+import com.jeesite.modules.mes.service.MesBatchSnService;
+import com.jeesite.modules.mes.service.MesErpPrdInstockLogService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * ERP 生产入库单 Save(PRD_INSTOCK)。
+ * 数量由 MES 批次码 mes_batch_sn.nums 汇总;ERP 批号 FLot 由 ERP 系统内部生成,Save 后回查。
+ */
+@Service
+public class ErpPrdInStockSaveService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpPrdInStockSaveService.class);
+	private static final String FORM_ID = "PRD_INSTOCK";
+	private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+	private final ErpProductionOrderService erpProductionOrderService;
+	private final ErpProductionMaterialService erpProductionMaterialService;
+	private final ErpPrdInStockService erpPrdInStockService;
+	private final ErpStockLocService erpStockLocService;
+	private final MesBatchSnService mesBatchSnService;
+	private final MesErpPrdInstockLogService mesErpPrdInstockLogService;
+
+	public ErpPrdInStockSaveService(KingdeeErpClient erpClient,
+			KingdeeErpProperties erpProperties,
+			ErpProductionOrderService erpProductionOrderService,
+			ErpProductionMaterialService erpProductionMaterialService,
+			ErpPrdInStockService erpPrdInStockService,
+			ErpStockLocService erpStockLocService,
+			MesBatchSnService mesBatchSnService,
+			MesErpPrdInstockLogService mesErpPrdInstockLogService) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+		this.erpProductionOrderService = erpProductionOrderService;
+		this.erpProductionMaterialService = erpProductionMaterialService;
+		this.erpPrdInStockService = erpPrdInStockService;
+		this.erpStockLocService = erpStockLocService;
+		this.mesBatchSnService = mesBatchSnService;
+		this.mesErpPrdInstockLogService = mesErpPrdInstockLogService;
+	}
+
+	@Transactional
+	public ErpPrdInStockResult submit(ErpPrdInStockSubmitRequest request) {
+		if (request == null || StringUtils.isBlank(request.getMoBillNo())) {
+			return ErpPrdInStockResult.fail("生产订单号不能为空");
+		}
+		if (StringUtils.isBlank(request.getEntryId())) {
+			return ErpPrdInStockResult.fail("生产订单分录不能为空");
+		}
+		if (request.getBatchIds() == null || request.getBatchIds().isEmpty()) {
+			return ErpPrdInStockResult.fail("请至少选择一个 MES 批次码");
+		}
+
+		ErpProductionOrder moEntry = erpProductionOrderService.findInstockMoEntry(
+				request.getMoBillNo().trim(), request.getEntryId().trim());
+		if (moEntry == null) {
+			return ErpPrdInStockResult.fail("未找到对应的生产订单分录");
+		}
+		if (!erpProperties.isAllowedPrdInStockMaterial(moEntry.getMaterialNumber())) {
+			return ErpPrdInStockResult.fail("该订单分录物料不在 HEV/115 入库范围内");
+		}
+
+		List<MesBatchSn> batches = loadAndValidateBatches(request.getBatchIds(), moEntry.getMaterialNumber());
+		if (batches.isEmpty()) {
+			return ErpPrdInStockResult.fail("未找到有效的待入库批次码");
+		}
+		BigDecimal totalQty = sumBatchQty(batches);
+		if (totalQty.compareTo(BigDecimal.ZERO) <= 0) {
+			return ErpPrdInStockResult.fail("所选批次码合计数量必须大于 0");
+		}
+
+		String stockNumber = StringUtils.trim(request.getStockNumber());
+		if (StringUtils.isBlank(stockNumber)) {
+			stockNumber = firstNonBlank(moEntry.getStockNumber(), erpProperties.getPrdInStockDefaultStockNumber(),
+					erpProperties.getInStockStockNumber());
+		}
+		if (StringUtils.isBlank(stockNumber)) {
+			return ErpPrdInStockResult.fail("请选择入库仓库");
+		}
+		String stockLocNumber = StringUtils.trim(request.getStockLocNumber());
+		if (erpStockLocService.requiresStockLocation(stockNumber) && StringUtils.isBlank(stockLocNumber)) {
+			return ErpPrdInStockResult.fail("所选仓库需要选择仓位");
+		}
+
+		String unitNumber = resolveUnitNumber(moEntry);
+		if (StringUtils.isBlank(unitNumber)) {
+			return ErpPrdInStockResult.fail("物料单位编码为空,无法入库");
+		}
+
+		JSONObject saveData = buildSaveData(moEntry, totalQty, unitNumber, stockNumber, stockLocNumber);
+		logger.info("ERP production in-stock save request: {}", saveData.toJSONString());
+		JSONObject response = erpClient.save(FORM_ID, saveData);
+		ErpPrdInStockResult result = parseSaveResponse(response, totalQty, batches.size());
+		if (!result.isSuccess()) {
+			return result;
+		}
+
+		String erpLotNumber = resolveErpLotNumber(result.getBillNo());
+		if (StringUtils.isNotBlank(erpLotNumber)) {
+			result = ErpPrdInStockResult.success(result.getBillNo(), result.getBillId(), erpLotNumber,
+					totalQty, batches.size());
+		}
+
+		Date now = new Date();
+		mesBatchSnService.markInstocked(batches, moEntry.getBillNo(), moEntry.getEntryId(),
+				result.getBillNo(), result.getLotNumber(), now);
+		saveInstockLog(moEntry, batches, result, stockNumber, stockLocNumber, totalQty, now);
+		return result;
+	}
+
+	/** 加载并校验 MES 批次码:nums>0、未入库、产线与物料匹配 */
+	private List<MesBatchSn> loadAndValidateBatches(List<String> batchIds, String materialNumber) {
+		List<MesBatchSn> batches = mesBatchSnService.findByIds(batchIds);
+		List<MesBatchSn> valid = new ArrayList<>();
+		for (MesBatchSn batch : batches) {
+			if (batch == null) {
+				continue;
+			}
+			if (batch.getNums() == null || batch.getNums() <= 0) {
+				continue;
+			}
+			if ("1".equals(StringUtils.trim(batch.getInstockStatus()))) {
+				continue;
+			}
+			if (!MesErpLineMaterialMapping.isLineAllowedForMaterial(batch.getLineSn(), materialNumber)) {
+				continue;
+			}
+			valid.add(batch);
+		}
+		return valid;
+	}
+
+	private BigDecimal sumBatchQty(List<MesBatchSn> batches) {
+		BigDecimal total = BigDecimal.ZERO;
+		for (MesBatchSn batch : batches) {
+			if (batch.getNums() != null) {
+				total = total.add(BigDecimal.valueOf(batch.getNums()));
+			}
+		}
+		return ErpKingdeeParseUtils.normalizeQty(total);
+	}
+
+	private String resolveUnitNumber(ErpProductionOrder moEntry) {
+		if (StringUtils.isNotBlank(moEntry.getUnitNumber())) {
+			return moEntry.getUnitNumber().trim();
+		}
+		return erpProductionMaterialService.findMaterialUnitNumber(moEntry.getMaterialNumber());
+	}
+
+	private JSONObject buildSaveData(ErpProductionOrder moEntry, BigDecimal totalQty, String unitNumber,
+			String stockNumber, String stockLocNumber) {
+		String orgNumber = firstNonBlank(moEntry.getPrdOrgNumber(), erpProperties.getOrgNumber(), "110");
+		String ownerType = erpProperties.getPrdInStockOwnerType();
+		String keeperType = erpProperties.getPrdInStockKeeperType();
+		String stockStatus = erpProperties.getPrdInStockStockStatus();
+		Object erpQty = ErpKingdeeParseUtils.toErpQty(totalQty);
+
+		JSONObject model = new JSONObject(true);
+		model.put("FID", 0);
+		model.put("FBillType", numberObject(erpProperties.getPrdInStockBillType(), true));
+		model.put("FDate", LocalDate.now().format(DATE_FORMAT));
+		model.put("FPrdOrgId", numberObject(orgNumber, false));
+		model.put("FStockOrgId", numberObject(orgNumber, false));
+		model.put("FOwnerTypeId0", ownerType);
+		model.put("FOwnerId0", numberObject(orgNumber, false));
+		if (StringUtils.isNotBlank(moEntry.getWorkshopNumber())) {
+			model.put("FWorkShopId", numberObject(moEntry.getWorkshopNumber(), false));
+		}
+
+		JSONObject entity = new JSONObject(true);
+		entity.put("FEntryID", 0);
+		entity.put("FMaterialId", numberObject(moEntry.getMaterialNumber(), false));
+		entity.put("FInStockType", erpProperties.getPrdInStockInStockType());
+		entity.put("FUnitID", numberObject(unitNumber, false));
+		entity.put("FBaseUnitId", numberObject(unitNumber, false));
+		entity.put("FMustQty", erpQty);
+		entity.put("FRealQty", erpQty);
+		entity.put("FBaseMustQty", erpQty);
+		entity.put("FBaseRealQty", erpQty);
+		entity.put("FStockRealQty", erpQty);
+		entity.put("FMoBillNo", moEntry.getBillNo());
+		entity.put("FMoId", parseLong(moEntry.getFid()));
+		entity.put("FMoEntryId", parseLong(moEntry.getEntryId()));
+		if (StringUtils.isNotBlank(moEntry.getSeq())) {
+			entity.put("FMoEntrySeq", parseInt(moEntry.getSeq()));
+		}
+		entity.put("FOwnerTypeId", ownerType);
+		entity.put("FOwnerId", numberObject(orgNumber, false));
+		entity.put("FKeeperTypeId", keeperType);
+		entity.put("FKeeperId", numberObject(orgNumber, false));
+		entity.put("FStockId", numberObject(stockNumber, false));
+		entity.put("FStockStatusId", numberObject(stockStatus, false));
+		putStockLoc(entity, stockNumber, stockLocNumber);
+		// 不传 FLot,由 ERP 系统内部生成批号
+
+		JSONArray entityList = new JSONArray();
+		entityList.add(entity);
+		model.put("FEntity", entityList);
+
+		JSONObject saveData = new JSONObject(true);
+		saveData.put("NeedUpDateFields", new JSONArray());
+		saveData.put("NeedReturnFields", JSONArray.parseArray("[\"FBillNo\"]"));
+		saveData.put("IsDeleteEntry", "true");
+		saveData.put("IsVerifyBaseDataField", "false");
+		saveData.put("IsEntryBatchFill", "true");
+		saveData.put("ValidateFlag", "true");
+		saveData.put("NumberSearch", "true");
+		saveData.put("IsAutoAdjustField", "true");
+		saveData.put("IgnoreInterationFlag", "true");
+		saveData.put("Model", model);
+		return saveData;
+	}
+
+	private void putStockLoc(JSONObject entry, String stockNumber, String stockLocNumber) {
+		if (!erpStockLocService.requiresStockLocation(stockNumber)) {
+			return;
+		}
+		String locNumber = StringUtils.trim(stockLocNumber);
+		if (StringUtils.isBlank(locNumber)) {
+			return;
+		}
+		String locKey = erpStockLocService.resolveLocKeyForStock(stockNumber);
+		String saveKey = erpProperties.toStockLocSaveKey(locKey);
+		JSONObject flexValue = numberObject(locNumber, false);
+		JSONObject stockLoc = new JSONObject(true);
+		stockLoc.put(saveKey, flexValue);
+		entry.put("FStockLocId", stockLoc);
+	}
+
+	/** Save 成功后 BillQuery 回查 ERP 内部批号 */
+	private String resolveErpLotNumber(String billNo) {
+		if (StringUtils.isBlank(billNo)) {
+			return "";
+		}
+		List<ErpPrdInStock> rows = erpPrdInStockService.findDetailList(billNo.trim());
+		for (ErpPrdInStock row : rows) {
+			if (StringUtils.isNotBlank(row.getLotNumber())) {
+				return row.getLotNumber().trim();
+			}
+		}
+		return "";
+	}
+
+	private void saveInstockLog(ErpProductionOrder moEntry, List<MesBatchSn> batches, ErpPrdInStockResult result,
+			String stockNumber, String stockLocNumber, BigDecimal totalQty, Date now) {
+		Set<String> batchSnSet = new LinkedHashSet<>();
+		for (MesBatchSn batch : batches) {
+			if (batch != null && StringUtils.isNotBlank(batch.getBatchSn())) {
+				batchSnSet.add(batch.getBatchSn().trim());
+			}
+		}
+		MesErpPrdInstockLog log = new MesErpPrdInstockLog();
+		log.setIsNewRecord(true);
+		log.setMoBillNo(moEntry.getBillNo());
+		log.setMoEntryId(moEntry.getEntryId());
+		log.setMaterialNumber(moEntry.getMaterialNumber());
+		log.setErpBillNo(result.getBillNo());
+		log.setErpLotNumber(result.getLotNumber());
+		log.setTotalQty(totalQty);
+		log.setBatchCount(batches.size());
+		log.setStockNumber(stockNumber);
+		log.setStockLocNumber(stockLocNumber);
+		log.setBatchSnList(batchSnSet.stream().collect(Collectors.joining(",")));
+		log.setStatus("0");
+		log.setCreateDate(now);
+		log.setUpdateDate(now);
+		mesErpPrdInstockLogService.saveLog(log);
+	}
+
+	private JSONObject numberObject(String number, boolean useFNumberKey) {
+		JSONObject object = new JSONObject();
+		object.put(useFNumberKey ? "FNUMBER" : "FNumber", number);
+		return object;
+	}
+
+	private long parseLong(String value) {
+		if (StringUtils.isBlank(value)) {
+			return 0L;
+		}
+		return Long.parseLong(value.trim());
+	}
+
+	private int parseInt(String value) {
+		if (StringUtils.isBlank(value)) {
+			return 0;
+		}
+		return Integer.parseInt(value.trim());
+	}
+
+	private String firstNonBlank(String... values) {
+		for (String value : values) {
+			if (StringUtils.isNotBlank(value)) {
+				return value.trim();
+			}
+		}
+		return "";
+	}
+
+	private ErpPrdInStockResult parseSaveResponse(JSONObject response, BigDecimal totalQty, int batchCount) {
+		if (response == null) {
+			return ErpPrdInStockResult.fail("ERP 返回为空");
+		}
+		JSONObject resultWrapper = response.getJSONObject("Result");
+		JSONObject status = resultWrapper == null ? null : resultWrapper.getJSONObject("ResponseStatus");
+		if (status != null && Boolean.TRUE.equals(status.getBoolean("IsSuccess"))) {
+			JSONObject result = resultWrapper.getJSONObject("Result");
+			String billNo = result == null ? "" : result.getString("Number");
+			if (StringUtils.isBlank(billNo)) {
+				billNo = resultWrapper.getString("Number");
+			}
+			String billId = result == null ? "" : result.getString("Id");
+			if (StringUtils.isBlank(billId)) {
+				billId = resultWrapper.getString("Id");
+			}
+			return ErpPrdInStockResult.success(billNo, billId, "", totalQty, batchCount);
+		}
+		return ErpPrdInStockResult.fail(extractErrorMessage(status, response));
+	}
+
+	private String extractErrorMessage(JSONObject status, JSONObject response) {
+		if (status != null) {
+			JSONArray errors = status.getJSONArray("Errors");
+			if (errors != null && !errors.isEmpty()) {
+				StringBuilder builder = new StringBuilder();
+				for (int i = 0; i < errors.size(); i++) {
+					JSONObject error = errors.getJSONObject(i);
+					if (builder.length() > 0) {
+						builder.append(";");
+					}
+					builder.append(error.getString("Message"));
+				}
+				if (builder.length() > 0) {
+					return builder.toString();
+				}
+			}
+		}
+		return "ERP 生产入库单保存失败:" + response.toJSONString();
+	}
+}

+ 547 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockService.java

@@ -0,0 +1,547 @@
+package com.jeesite.modules.mes.erp;
+
+
+
+import com.alibaba.fastjson.JSONArray;
+
+import com.alibaba.fastjson.JSONObject;
+
+import com.jeesite.common.lang.StringUtils;
+
+import org.slf4j.Logger;
+
+import org.slf4j.LoggerFactory;
+
+import org.springframework.stereotype.Service;
+
+
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.math.BigDecimal;
+
+
+
+/**
+
+ * ERP 生产入库单 BillQuery(PRD_INSTOCK)。
+
+ * 与直接调拨单一致,固定仅查 HEV / 115 两种物料编码。
+
+ */
+
+@Service
+
+public class ErpPrdInStockService {
+
+
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpPrdInStockService.class);
+
+	private static final String FORM_ID = "PRD_INSTOCK";
+
+	private static final String MO_BILL_NO_FIELD = "FMoBillNo";
+
+	private static final String MATERIAL_NUMBER_FIELD = "FMaterialId.FNUMBER";
+
+	private static final String DETAIL_FIELD_KEYS = String.join(",",
+
+			"FID",
+
+			"FBILLNO",
+
+			"FDate",
+
+			"FDocumentStatus",
+
+			"FPrdOrgId.FNUMBER",
+
+			"FPrdOrgId.FNAME",
+
+			MO_BILL_NO_FIELD,
+
+			"FEntity_FEntryID",
+
+			"FEntity_FSeq",
+
+			MATERIAL_NUMBER_FIELD,
+
+			"FMaterialName",
+
+			"FUnitID.FNAME",
+
+			"FUnitID.FNUMBER",
+
+			"FRealQty",
+
+			"FStockId.FNUMBER",
+
+			"FStockId.FNAME",
+
+			"FLot.FNUMBER");
+
+
+
+	private final KingdeeErpClient erpClient;
+
+	private final KingdeeErpProperties erpProperties;
+
+
+
+	public ErpPrdInStockService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties) {
+
+		this.erpClient = erpClient;
+
+		this.erpProperties = erpProperties;
+
+	}
+
+
+
+	/**
+
+	 * 分页查询生产入库明细分录(仅 HEV / 115 物料)。
+
+	 */
+
+	public List<ErpPrdInStock> findPageList(ErpPrdInStock query, int startRow, int limit) {
+
+		return findPageListWithFieldKeys(query, DETAIL_FIELD_KEYS, startRow, limit);
+
+	}
+
+
+
+	/**
+
+	 * 指定入库单号的全部分录(仍限定 HEV / 115 物料)。
+
+	 */
+
+	public List<ErpPrdInStock> findDetailList(String billNo) {
+
+		if (StringUtils.isBlank(billNo)) {
+
+			return new ArrayList<>();
+
+		}
+
+		ErpPrdInStock query = new ErpPrdInStock();
+
+		query.setBillNo(billNo.trim());
+
+		return findPageList(query, 0, 2000);
+
+	}
+
+
+
+	/**
+
+	 * 按生产订单号查询关联的生产入库明细分录(仅 HEV / 115 物料)。
+
+	 */
+
+	public List<ErpPrdInStock> findListByMoBillNo(String moBillNo) {
+
+		if (StringUtils.isBlank(moBillNo)) {
+
+			return new ArrayList<>();
+
+		}
+
+		ErpPrdInStock query = new ErpPrdInStock();
+
+		query.setMoBillNoQuery(moBillNo.trim());
+
+		return findPageList(query, 0, 2000);
+
+	}
+
+
+
+	/**
+
+	 * 直接调拨可选的生产入库行(与 {@link #findListByMoBillNo} 相同,BillQuery 已按物料过滤)。
+
+	 */
+
+	public List<ErpPrdInStock> findTransferSourceByMoBillNo(String moBillNo) {
+
+		return findListByMoBillNo(moBillNo);
+
+	}
+
+	/**
+	 * 按生产订单分录汇总已审核生产入库实收数量(PRD_INSTOCK.FRealQty)。
+	 */
+	public Map<String, BigDecimal> sumAuditedInstockQtyByMoEntry(String moBillNo) {
+		Map<String, BigDecimal> qtyMap = new HashMap<>();
+		if (StringUtils.isBlank(moBillNo)) {
+			return qtyMap;
+		}
+		List<ErpPrdInStock> lines = queryInstockLinesForMoEntrySum(moBillNo.trim());
+		for (ErpPrdInStock line : lines) {
+			if (line == null || line.getRealQty() == null) {
+				continue;
+			}
+			String moEntryId = StringUtils.trim(line.getMoEntryId());
+			if (StringUtils.isBlank(moEntryId)) {
+				continue;
+			}
+			qtyMap.merge(moEntryId, line.getRealQty(), BigDecimal::add);
+		}
+		return qtyMap;
+	}
+
+	private List<ErpPrdInStock> queryInstockLinesForMoEntrySum(String moBillNo) {
+		ErpPrdInStock query = new ErpPrdInStock();
+		query.setMoBillNoQuery(moBillNo);
+		query.setDocumentStatusQuery("C");
+		String fieldKeys = DETAIL_FIELD_KEYS + ",FMoEntryId";
+		try {
+			return findPageListWithFieldKeys(query, fieldKeys, 0, 5000);
+		} catch (IllegalStateException ex) {
+			if (isBillQueryFieldError(ex)) {
+				logger.warn("ERP 生产入库 BillQuery 字段 FMoEntryId 不可用,已入库数量按 0 展示:{}", ex.getMessage());
+				return new ArrayList<>();
+			}
+			throw ex;
+		}
+	}
+
+	private List<ErpPrdInStock> findPageListWithFieldKeys(ErpPrdInStock query, String fieldKeys, int startRow, int limit) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", fieldKeys);
+		data.put("FilterString", buildFilterString(query));
+		data.put("OrderString", "FDate DESC,FBILLNO DESC,FEntity_FSeq");
+		data.put("StartRow", Math.max(startRow, 0));
+		data.put("Limit", Math.max(limit, 1));
+		logger.info("ERP production in-stock query: {}", data.toJSONString());
+		return mapRows(erpClient.billQuery(data));
+	}
+
+	private boolean isBillQueryFieldError(IllegalStateException ex) {
+		String message = ex.getMessage();
+		return message != null && message.contains("不存在");
+	}
+
+	/**
+	 * 为生产订单分录填充已审核入库数量与未入库数量。
+	 */
+	public void enrichMoInstockProgress(String moBillNo, List<ErpProductionOrder> details) {
+		if (StringUtils.isBlank(moBillNo) || details == null || details.isEmpty()) {
+			return;
+		}
+		Map<String, BigDecimal> instockMap = sumAuditedInstockQtyByMoEntry(moBillNo.trim());
+		for (ErpProductionOrder detail : details) {
+			if (detail == null) {
+				continue;
+			}
+			BigDecimal moQty = ErpKingdeeParseUtils.normalizeQty(detail.getQty());
+			String entryId = StringUtils.trim(detail.getEntryId());
+			BigDecimal inQty = ErpKingdeeParseUtils.normalizeQty(instockMap.getOrDefault(entryId, BigDecimal.ZERO));
+			detail.setInstockQty(inQty);
+			detail.setRemainQty(ErpKingdeeParseUtils.normalizeQty(moQty.subtract(inQty)));
+		}
+	}
+
+
+
+	private String buildFilterString(ErpPrdInStock query) {
+
+		String filter = appendAnd(buildPrdOrgFilter(), buildAllowedMaterialInFilter());
+
+		if (query == null) {
+
+			return filter;
+
+		}
+
+		if (StringUtils.isNotBlank(query.getBillNoQuery())) {
+
+			filter = appendAnd(filter, "FBILLNO like '%" + escape(query.getBillNoQuery()) + "%'");
+
+		} else if (StringUtils.isNotBlank(query.getBillNo())) {
+
+			filter = appendAnd(filter, "FBILLNO='" + escape(query.getBillNo()) + "'");
+
+		}
+
+		if (StringUtils.isNotBlank(query.getMoBillNoQuery())) {
+
+			filter = appendAnd(filter, MO_BILL_NO_FIELD + "='" + escape(query.getMoBillNoQuery()) + "'");
+
+		}
+
+		if (StringUtils.isNotBlank(query.getDocumentStatusQuery())) {
+
+			filter = appendAnd(filter, "FDocumentStatus='" + escape(query.getDocumentStatusQuery()) + "'");
+
+		}
+
+		String materialFilter = buildMaterialFilter(query.getMaterialNumberQuery());
+
+		if (StringUtils.isNotBlank(materialFilter)) {
+
+			filter = appendAnd(filter, materialFilter);
+
+		}
+
+		return filter;
+
+	}
+
+
+
+	/**
+
+	 * 物料范围:默认固定 HEV + 115;页面可选缩小到其中一个。
+
+	 */
+
+	private String buildMaterialFilter(String materialNumberQuery) {
+
+		if (StringUtils.isNotBlank(materialNumberQuery)) {
+
+			String trimmed = materialNumberQuery.trim();
+
+			if (erpProperties.isAllowedPrdInStockMaterial(trimmed)) {
+
+				return MATERIAL_NUMBER_FIELD + "='" + escape(trimmed) + "'";
+
+			}
+
+		}
+
+		return "";
+
+	}
+
+
+
+	private String buildAllowedMaterialInFilter() {
+
+		List<String> materials = erpProperties.getPrdInStockMaterialNumbers();
+
+		if (materials == null || materials.isEmpty()) {
+
+			return MATERIAL_NUMBER_FIELD + "='8.10.HEV.10.047696'";
+
+		}
+
+		StringBuilder builder = new StringBuilder(MATERIAL_NUMBER_FIELD + " IN(");
+
+		for (int i = 0; i < materials.size(); i++) {
+
+			if (i > 0) {
+
+				builder.append(",");
+
+			}
+
+			builder.append("'").append(escape(materials.get(i))).append("'");
+
+		}
+
+		builder.append(")");
+
+		return builder.toString();
+
+	}
+
+
+
+	private String buildPrdOrgFilter() {
+
+		if (!erpProperties.isFilterCurrentOrg()) {
+
+			return "";
+
+		}
+
+		if (StringUtils.isNotBlank(erpProperties.getOrgNumber())) {
+
+			return "FPrdOrgId.FNUMBER='" + escape(erpProperties.getOrgNumber()) + "'";
+
+		}
+
+		Long currentOrganizationId = erpClient.getCurrentOrganizationId();
+
+		if (currentOrganizationId == null) {
+
+			return "1=1";
+
+		}
+
+		return "FPrdOrgId=" + currentOrganizationId;
+
+	}
+
+
+
+	private List<ErpPrdInStock> mapRows(JSONArray rows) {
+
+		List<ErpPrdInStock> list = new ArrayList<>();
+
+		for (int i = 0; i < rows.size(); i++) {
+
+			list.add(toDetail(rows.getJSONObject(i), i));
+
+		}
+
+		return list;
+
+	}
+
+
+
+	private ErpPrdInStock toDetail(JSONObject row, int index) {
+
+		ErpPrdInStock item = new ErpPrdInStock();
+
+		item.setFid(readString(row, "FID"));
+
+		item.setBillNo(readString(row, "FBILLNO", "FBillNo"));
+
+		item.setEntryId(readString(row, "FEntity_FEntryID", "FEntity_FEntryId"));
+
+		item.setSeq(readString(row, "FEntity_FSeq"));
+
+		String rowId = item.getBillNo();
+
+		if (StringUtils.isNotBlank(item.getEntryId())) {
+
+			rowId = rowId + "-" + item.getEntryId();
+
+		} else if (index >= 0) {
+
+			rowId = rowId + "-" + index;
+
+		}
+
+		item.setId(rowId);
+
+		item.setDocumentStatus(readString(row, "FDocumentStatus"));
+
+		item.setDocumentStatusName(documentStatusName(item.getDocumentStatus()));
+
+		item.setDate(formatErpDate(readString(row, "FDate")));
+
+		item.setPrdOrgNumber(readString(row, "FPrdOrgId.FNUMBER", "FPrdOrgId.FNumber"));
+
+		item.setPrdOrgName(readString(row, "FPrdOrgId.FNAME", "FPrdOrgId.FName"));
+
+		item.setMoBillNo(readString(row, MO_BILL_NO_FIELD, "FMoBillNo"));
+
+		item.setMoEntryId(readString(row, "FMoEntryId", "FMoEntryID", "FMOEntryId", "FMOEntryID"));
+
+		item.setMaterialNumber(readString(row, MATERIAL_NUMBER_FIELD, "FMaterialId.FNumber"));
+
+		item.setMaterialName(readString(row, "FMaterialName", "FMaterialId.FNAME", "FMaterialId.FName"));
+
+		item.setUnitName(readString(row, "FUnitID.FNAME", "FUnitId.FNAME", "FUnitId.FName"));
+
+		item.setUnitNumber(readString(row, "FUnitID.FNUMBER", "FUnitID.FNumber", "FUnitId.FNUMBER", "FUnitId.FNumber"));
+
+		item.setRealQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FRealQty")));
+
+		item.setStockNumber(readString(row, "FStockId.FNUMBER", "FStockId.FNumber"));
+
+		item.setStockName(readString(row, "FStockId.FNAME", "FStockId.FName"));
+
+		item.setLotNumber(readString(row, "FLot.FNUMBER", "FLot.FNumber"));
+
+		return item;
+
+	}
+
+
+
+	private String appendAnd(String left, String right) {
+
+		if (StringUtils.isBlank(left)) {
+
+			return right;
+
+		}
+
+		if (StringUtils.isBlank(right)) {
+
+			return left;
+
+		}
+
+		return left + " AND " + right;
+
+	}
+
+
+
+	private String escape(String value) {
+
+		return value == null ? "" : value.trim().replace("'", "''");
+
+	}
+
+
+
+	private String readString(JSONObject row, String... keys) {
+
+		return ErpKingdeeParseUtils.readString(row, keys);
+
+	}
+
+
+
+	private String formatErpDate(String rawValue) {
+
+		if (StringUtils.isBlank(rawValue)) {
+
+			return "";
+
+		}
+
+		String trimmed = rawValue.trim();
+
+		return trimmed.length() >= 10 ? trimmed.substring(0, 10) : trimmed;
+
+	}
+
+
+
+	private String documentStatusName(String status) {
+
+		if ("A".equals(status)) {
+
+			return "创建";
+
+		}
+
+		if ("B".equals(status)) {
+
+			return "审核中";
+
+		}
+
+		if ("C".equals(status)) {
+
+			return "已审核";
+
+		}
+
+		if ("D".equals(status)) {
+
+			return "重新审核";
+
+		}
+
+		return status;
+
+	}
+
+}
+

+ 61 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPrdInStockSubmitRequest.java

@@ -0,0 +1,61 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 提交 ERP 生产入库单(PRD_INSTOCK)请求。
+ * 数量由所选 MES 批次码(mes_batch_sn.nums)汇总,不传手工数量。
+ */
+public class ErpPrdInStockSubmitRequest implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String moBillNo;
+	private String entryId;
+	/** mes_batch_sn 主键 id 列表 */
+	private List<String> batchIds = new ArrayList<>();
+	private String stockNumber;
+	private String stockLocNumber;
+
+	public String getMoBillNo() {
+		return moBillNo;
+	}
+
+	public void setMoBillNo(String moBillNo) {
+		this.moBillNo = moBillNo;
+	}
+
+	public String getEntryId() {
+		return entryId;
+	}
+
+	public void setEntryId(String entryId) {
+		this.entryId = entryId;
+	}
+
+	public List<String> getBatchIds() {
+		return batchIds;
+	}
+
+	public void setBatchIds(List<String> batchIds) {
+		this.batchIds = batchIds;
+	}
+
+	public String getStockNumber() {
+		return stockNumber;
+	}
+
+	public void setStockNumber(String stockNumber) {
+		this.stockNumber = stockNumber;
+	}
+
+	public String getStockLocNumber() {
+		return stockLocNumber;
+	}
+
+	public void setStockLocNumber(String stockLocNumber) {
+		this.stockLocNumber = stockLocNumber;
+	}
+}

+ 56 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpProductionOrder.java

@@ -17,7 +17,9 @@ public class ErpProductionOrder implements Serializable {
 	private String documentStatusName;
 	private String date;
 	private String prdOrgName;
+	private String prdOrgNumber;
 	private String workshopName;
+	private String workshopNumber;
 	private String plannerName;
 	private String inventoryCategory;
 	private Integer entryCount;
@@ -29,6 +31,8 @@ public class ErpProductionOrder implements Serializable {
 	private String specification;
 	private BigDecimal qty;
 	private String unitName;
+	private String unitNumber;
+	private String stockNumber;
 	private String planStartDate;
 	private String planFinishDate;
 	private String businessStatus;
@@ -38,6 +42,10 @@ public class ErpProductionOrder implements Serializable {
 	private String routingNumber;
 	private String projectNo;
 	private String mtoNo;
+	/** 已审核生产入库合计(选单页计算) */
+	private BigDecimal instockQty;
+	/** 未入库数量 = 订单数量 - 已审核入库(选单页计算) */
+	private BigDecimal remainQty;
 
 	public String getId() {
 		return id;
@@ -95,6 +103,14 @@ public class ErpProductionOrder implements Serializable {
 		this.prdOrgName = prdOrgName;
 	}
 
+	public String getPrdOrgNumber() {
+		return prdOrgNumber;
+	}
+
+	public void setPrdOrgNumber(String prdOrgNumber) {
+		this.prdOrgNumber = prdOrgNumber;
+	}
+
 	public String getWorkshopName() {
 		return workshopName;
 	}
@@ -103,6 +119,14 @@ public class ErpProductionOrder implements Serializable {
 		this.workshopName = workshopName;
 	}
 
+	public String getWorkshopNumber() {
+		return workshopNumber;
+	}
+
+	public void setWorkshopNumber(String workshopNumber) {
+		this.workshopNumber = workshopNumber;
+	}
+
 	public String getPlannerName() {
 		return plannerName;
 	}
@@ -191,6 +215,22 @@ public class ErpProductionOrder implements Serializable {
 		this.unitName = unitName;
 	}
 
+	public String getUnitNumber() {
+		return unitNumber;
+	}
+
+	public void setUnitNumber(String unitNumber) {
+		this.unitNumber = unitNumber;
+	}
+
+	public String getStockNumber() {
+		return stockNumber;
+	}
+
+	public void setStockNumber(String stockNumber) {
+		this.stockNumber = stockNumber;
+	}
+
 	public String getPlanStartDate() {
 		return planStartDate;
 	}
@@ -262,4 +302,20 @@ public class ErpProductionOrder implements Serializable {
 	public void setMtoNo(String mtoNo) {
 		this.mtoNo = mtoNo;
 	}
+
+	public BigDecimal getInstockQty() {
+		return instockQty;
+	}
+
+	public void setInstockQty(BigDecimal instockQty) {
+		this.instockQty = instockQty;
+	}
+
+	public BigDecimal getRemainQty() {
+		return remainQty;
+	}
+
+	public void setRemainQty(BigDecimal remainQty) {
+		this.remainQty = remainQty;
+	}
 }

+ 167 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpProductionOrderService.java

@@ -116,6 +116,113 @@ public class ErpProductionOrderService {
 		return findAggregatedPageListByView(billNo, startRow, limit);
 	}
 
+	/**
+	 * 生产入库选单:仅含配置物料(HEV/115 具体编码)的生产订单,按单号汇总。
+	 */
+	public List<ErpProductionOrder> findPrdInStockOrderPageList(String billNo, int startRow, int limit) {
+		List<String> materialNumbers = erpProperties.getPrdInStockMaterialNumbers();
+		if (materialNumbers.isEmpty()) {
+			return new ArrayList<>();
+		}
+		return findAggregatedPageListByMaterialNumbers(billNo, startRow, limit, materialNumbers);
+	}
+
+	private List<ErpProductionOrder> findAggregatedPageListByMaterialNumbers(String billNo, int startRow, int limit,
+			List<String> materialNumbers) {
+		int targetSize = startRow + limit;
+		int fetchStart = 0;
+		int fetchLimit = Math.max(limit * 3, 50);
+		int maxFetchRows = Math.max(targetSize * 5, 200);
+		Map<String, ErpProductionOrder> orderMap = new LinkedHashMap<>();
+
+		while (orderMap.size() < targetSize && fetchStart < maxFetchRows) {
+			List<ErpProductionOrder> chunk = findDetailListByMaterialNumbers(billNo, materialNumbers, fetchStart, fetchLimit);
+			if (chunk.isEmpty()) {
+				break;
+			}
+			for (ErpProductionOrder detail : chunk) {
+				if (!erpProperties.isAllowedPrdInStockMaterial(detail.getMaterialNumber())) {
+					continue;
+				}
+				ErpProductionOrder order = orderMap.get(detail.getBillNo());
+				if (order == null) {
+					order = toHeaderFromDetail(detail);
+					order.setEntryCount(0);
+					order.setTotalQty(BigDecimal.ZERO);
+					order.setInventoryCategory(resolveInventoryCategoryLabel(detail.getMaterialNumber()));
+					orderMap.put(detail.getBillNo(), order);
+				}
+				order.setEntryCount(order.getEntryCount() + 1);
+				if (detail.getQty() != null) {
+					order.setTotalQty(order.getTotalQty().add(detail.getQty()));
+				}
+				mergeHeaderFieldsFromDetail(order, detail);
+			}
+			if (chunk.size() < fetchLimit) {
+				break;
+			}
+			fetchStart += fetchLimit;
+		}
+
+		List<ErpProductionOrder> allOrders = new ArrayList<>(orderMap.values());
+		if (startRow >= allOrders.size()) {
+			return new ArrayList<>();
+		}
+		return new ArrayList<>(allOrders.subList(startRow, Math.min(startRow + limit, allOrders.size())));
+	}
+
+	private List<ErpProductionOrder> findDetailListByMaterialNumbers(String billNo, List<String> materialNumbers,
+			int startRow, int limit) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", DETAIL_FIELD_KEYS);
+		data.put("FilterString", buildMaterialNumberFilter(billNo, materialNumbers));
+		data.put("OrderString", "FDate DESC,FBILLNO DESC,FTreeEntity_FSeq");
+		data.put("StartRow", Math.max(startRow, 0));
+		data.put("Limit", Math.max(limit, 1));
+		logger.info("ERP production order prd-instock query: {}", data.toJSONString());
+
+		JSONArray rows = erpClient.billQuery(data);
+		List<ErpProductionOrder> list = new ArrayList<>();
+		for (int i = 0; i < rows.size(); i++) {
+			list.add(toDetail(rows.getJSONObject(i), i));
+		}
+		return list;
+	}
+
+	private String buildMaterialNumberFilter(String billNo, List<String> materialNumbers) {
+		List<String> parts = new ArrayList<>();
+		for (String materialNumber : materialNumbers) {
+			if (StringUtils.isBlank(materialNumber)) {
+				continue;
+			}
+			parts.add("FMATERIALID.FNUMBER='" + materialNumber.trim().replace("'", "''") + "'");
+		}
+		if (parts.isEmpty()) {
+			return buildFilterString(billNo);
+		}
+		String materialFilter = "(" + String.join(" OR ", parts) + ")";
+		String baseFilter = appendAnd(buildCurrentOrgFilter(), materialFilter);
+		if (StringUtils.isBlank(billNo)) {
+			return baseFilter;
+		}
+		return appendAnd(baseFilter, "FBILLNO='" + billNo.trim().replace("'", "''") + "'");
+	}
+
+	private String resolveInventoryCategoryLabel(String materialNumber) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return "";
+		}
+		String trimmed = materialNumber.trim();
+		if (trimmed.contains("HEV")) {
+			return "HEV";
+		}
+		if (trimmed.contains("115")) {
+			return "\u6b23\u65fa\u8fbe115";
+		}
+		return "";
+	}
+
 	private List<ErpProductionOrder> findAggregatedPageListByBillQuery(String billNo, int startRow, int limit) {
 		int targetSize = startRow + limit;
 		int fetchStart = 0;
@@ -320,6 +427,66 @@ public class ErpProductionOrderService {
 		return findDetailListByBillNos(singletonList(billNo));
 	}
 
+	/**
+	 * 生产入库 Save 用:按订单号+分录内码取一行,含组织/车间/单位编码。
+	 */
+	public ErpProductionOrder findInstockMoEntry(String billNo, String entryId) {
+		if (StringUtils.isBlank(billNo) || StringUtils.isBlank(entryId)) {
+			return null;
+		}
+		for (ErpProductionOrder detail : findInstockMoDetailList(billNo.trim())) {
+			if (entryId.trim().equals(StringUtils.trim(detail.getEntryId()))) {
+				return detail;
+			}
+		}
+		return null;
+	}
+
+	private static final String INSTOCK_MO_FIELD_KEYS = String.join(",",
+			"FID",
+			"FBILLNO",
+			"FPrdOrgId.FNUMBER",
+			"FWorkShopID.FNUMBER",
+			"FTreeEntity_FEntryId",
+			"FTreeEntity_FSeq",
+			"FMATERIALID.FNUMBER",
+			"FMATERIALID.FNAME",
+			"FUnitId.FNUMBER",
+			"FUnitId.FNAME",
+			"FQTY",
+			"FStockId.FNUMBER");
+
+	private List<ErpProductionOrder> findInstockMoDetailList(String billNo) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", INSTOCK_MO_FIELD_KEYS);
+		data.put("FilterString", appendAnd(buildCurrentOrgFilter(), "FBILLNO='" + escapeBillNo(billNo) + "'"));
+		data.put("OrderString", "FTreeEntity_FSeq");
+		data.put("StartRow", 0);
+		data.put("Limit", 2000);
+		logger.info("ERP production order instock mo query: {}", data.toJSONString());
+		JSONArray rows = erpClient.billQuery(data);
+		List<ErpProductionOrder> list = new ArrayList<>();
+		for (int i = 0; i < rows.size(); i++) {
+			list.add(toInstockMoDetail(rows.getJSONObject(i), i));
+		}
+		return list;
+	}
+
+	private ErpProductionOrder toInstockMoDetail(JSONObject row, int index) {
+		ErpProductionOrder order = toDetail(row, index);
+		order.setPrdOrgNumber(ErpKingdeeParseUtils.readString(row, "FPrdOrgId.FNUMBER", "FPrdOrgId.FNumber"));
+		order.setWorkshopNumber(ErpKingdeeParseUtils.readString(row, "FWorkShopID.FNUMBER", "FWorkShopID.FNumber",
+				"FWorkShopID0.FNUMBER", "FWorkShopID0.FNumber"));
+		order.setUnitNumber(ErpKingdeeParseUtils.readString(row, "FUnitId.FNUMBER", "FUnitId.FNumber", "FUnitID.FNUMBER"));
+		order.setStockNumber(ErpKingdeeParseUtils.readString(row, "FStockId.FNUMBER", "FStockId.FNumber"));
+		return order;
+	}
+
+	private String escapeBillNo(String billNo) {
+		return billNo == null ? "" : billNo.trim().replace("'", "''");
+	}
+
 	private List<ErpProductionOrder> findHeaderList(String billNo, int startRow, int limit) {
 		JSONObject data = new JSONObject();
 		data.put("FormId", FORM_ID);

+ 280 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirect.java

@@ -0,0 +1,280 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * ERP 直接调拨单(STK_TransferDirect)BillQuery 数据。
+ */
+public class ErpTransferDirect implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String id;
+	private String fid;
+	private String billNo;
+	private String documentStatus;
+	private String documentStatusName;
+	private String date;
+	private String billTypeNumber;
+	private String billTypeName;
+	private String stockOrgNumber;
+	private String stockOrgName;
+	private String stockOutOrgNumber;
+	private String stockOutOrgName;
+	private String entryId;
+	private String seq;
+	private String materialNumber;
+	private String materialName;
+	private String unitName;
+	private BigDecimal qty;
+	private String srcStockNumber;
+	private String srcStockName;
+	private String destStockNumber;
+	private String destStockName;
+	private String lotNumber;
+	private String note;
+	/** 源单编号(BillQuery:FSrcBillNo) */
+	private String srcBillNo;
+	/** 订单号(BillQuery:FOrderNo) */
+	private String orderNo;
+
+	/** 查询条件:调拨单号(模糊) */
+	private String billNoQuery;
+	/** 查询条件:物料编码(限定 HEV / 115 范围内) */
+	private String materialNumberQuery;
+	/** 查询条件:单据状态 */
+	private String documentStatusQuery;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getFid() {
+		return fid;
+	}
+
+	public void setFid(String fid) {
+		this.fid = fid;
+	}
+
+	public String getBillNo() {
+		return billNo;
+	}
+
+	public void setBillNo(String billNo) {
+		this.billNo = billNo;
+	}
+
+	public String getDocumentStatus() {
+		return documentStatus;
+	}
+
+	public void setDocumentStatus(String documentStatus) {
+		this.documentStatus = documentStatus;
+	}
+
+	public String getDocumentStatusName() {
+		return documentStatusName;
+	}
+
+	public void setDocumentStatusName(String documentStatusName) {
+		this.documentStatusName = documentStatusName;
+	}
+
+	public String getDate() {
+		return date;
+	}
+
+	public void setDate(String date) {
+		this.date = date;
+	}
+
+	public String getBillTypeNumber() {
+		return billTypeNumber;
+	}
+
+	public void setBillTypeNumber(String billTypeNumber) {
+		this.billTypeNumber = billTypeNumber;
+	}
+
+	public String getBillTypeName() {
+		return billTypeName;
+	}
+
+	public void setBillTypeName(String billTypeName) {
+		this.billTypeName = billTypeName;
+	}
+
+	public String getStockOrgNumber() {
+		return stockOrgNumber;
+	}
+
+	public void setStockOrgNumber(String stockOrgNumber) {
+		this.stockOrgNumber = stockOrgNumber;
+	}
+
+	public String getStockOrgName() {
+		return stockOrgName;
+	}
+
+	public void setStockOrgName(String stockOrgName) {
+		this.stockOrgName = stockOrgName;
+	}
+
+	public String getStockOutOrgNumber() {
+		return stockOutOrgNumber;
+	}
+
+	public void setStockOutOrgNumber(String stockOutOrgNumber) {
+		this.stockOutOrgNumber = stockOutOrgNumber;
+	}
+
+	public String getStockOutOrgName() {
+		return stockOutOrgName;
+	}
+
+	public void setStockOutOrgName(String stockOutOrgName) {
+		this.stockOutOrgName = stockOutOrgName;
+	}
+
+	public String getEntryId() {
+		return entryId;
+	}
+
+	public void setEntryId(String entryId) {
+		this.entryId = entryId;
+	}
+
+	public String getSeq() {
+		return seq;
+	}
+
+	public void setSeq(String seq) {
+		this.seq = seq;
+	}
+
+	public String getMaterialNumber() {
+		return materialNumber;
+	}
+
+	public void setMaterialNumber(String materialNumber) {
+		this.materialNumber = materialNumber;
+	}
+
+	public String getMaterialName() {
+		return materialName;
+	}
+
+	public void setMaterialName(String materialName) {
+		this.materialName = materialName;
+	}
+
+	public String getUnitName() {
+		return unitName;
+	}
+
+	public void setUnitName(String unitName) {
+		this.unitName = unitName;
+	}
+
+	public BigDecimal getQty() {
+		return qty;
+	}
+
+	public void setQty(BigDecimal qty) {
+		this.qty = qty;
+	}
+
+	public String getSrcStockNumber() {
+		return srcStockNumber;
+	}
+
+	public void setSrcStockNumber(String srcStockNumber) {
+		this.srcStockNumber = srcStockNumber;
+	}
+
+	public String getSrcStockName() {
+		return srcStockName;
+	}
+
+	public void setSrcStockName(String srcStockName) {
+		this.srcStockName = srcStockName;
+	}
+
+	public String getDestStockNumber() {
+		return destStockNumber;
+	}
+
+	public void setDestStockNumber(String destStockNumber) {
+		this.destStockNumber = destStockNumber;
+	}
+
+	public String getDestStockName() {
+		return destStockName;
+	}
+
+	public void setDestStockName(String destStockName) {
+		this.destStockName = destStockName;
+	}
+
+	public String getLotNumber() {
+		return lotNumber;
+	}
+
+	public void setLotNumber(String lotNumber) {
+		this.lotNumber = lotNumber;
+	}
+
+	public String getNote() {
+		return note;
+	}
+
+	public void setNote(String note) {
+		this.note = note;
+	}
+
+	public String getSrcBillNo() {
+		return srcBillNo;
+	}
+
+	public void setSrcBillNo(String srcBillNo) {
+		this.srcBillNo = srcBillNo;
+	}
+
+	public String getOrderNo() {
+		return orderNo;
+	}
+
+	public void setOrderNo(String orderNo) {
+		this.orderNo = orderNo;
+	}
+
+	public String getBillNoQuery() {
+		return billNoQuery;
+	}
+
+	public void setBillNoQuery(String billNoQuery) {
+		this.billNoQuery = billNoQuery;
+	}
+
+	public String getMaterialNumberQuery() {
+		return materialNumberQuery;
+	}
+
+	public void setMaterialNumberQuery(String materialNumberQuery) {
+		this.materialNumberQuery = materialNumberQuery;
+	}
+
+	public String getDocumentStatusQuery() {
+		return documentStatusQuery;
+	}
+
+	public void setDocumentStatusQuery(String documentStatusQuery) {
+		this.documentStatusQuery = documentStatusQuery;
+	}
+}

+ 48 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectResult.java

@@ -0,0 +1,48 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+
+/**
+ * ERP 直接调拨单 Save 结果
+ */
+public class ErpTransferDirectResult implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private boolean success;
+	private String billNo;
+	private String billId;
+	private String message;
+
+	public static ErpTransferDirectResult success(String billNo, String billId) {
+		ErpTransferDirectResult result = new ErpTransferDirectResult();
+		result.success = true;
+		result.billNo = billNo;
+		result.billId = billId;
+		result.message = "直接调拨单保存成功";
+		return result;
+	}
+
+	public static ErpTransferDirectResult fail(String message) {
+		ErpTransferDirectResult result = new ErpTransferDirectResult();
+		result.success = false;
+		result.message = message;
+		return result;
+	}
+
+	public boolean isSuccess() {
+		return success;
+	}
+
+	public String getBillNo() {
+		return billNo;
+	}
+
+	public String getBillId() {
+		return billId;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+}

+ 310 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSaveService.java

@@ -0,0 +1,310 @@
+package com.jeesite.modules.mes.erp;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.jeesite.common.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * ERP 直接调拨单 Save(STK_TransferDirect)。
+ * 批号来自生产入库单,便于生产订单下查关联。
+ */
+@Service
+public class ErpTransferDirectSaveService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpTransferDirectSaveService.class);
+	private static final String FORM_ID = "STK_TransferDirect";
+	private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+	private final ErpPrdInStockService erpPrdInStockService;
+	private final ErpProductionMaterialService erpProductionMaterialService;
+	private final ErpStockLocService erpStockLocService;
+
+	public ErpTransferDirectSaveService(KingdeeErpClient erpClient,
+			KingdeeErpProperties erpProperties,
+			ErpPrdInStockService erpPrdInStockService,
+			ErpProductionMaterialService erpProductionMaterialService,
+			ErpStockLocService erpStockLocService) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+		this.erpPrdInStockService = erpPrdInStockService;
+		this.erpProductionMaterialService = erpProductionMaterialService;
+		this.erpStockLocService = erpStockLocService;
+	}
+
+	public ErpTransferDirectResult submit(ErpTransferDirectSubmitRequest request) {
+		if (request == null || StringUtils.isBlank(request.getMoBillNo())) {
+			return ErpTransferDirectResult.fail("生产订单号不能为空");
+		}
+		if (StringUtils.isBlank(request.getInstockEntryId())) {
+			return ErpTransferDirectResult.fail("请选择生产入库行");
+		}
+		BigDecimal qty = ErpKingdeeParseUtils.normalizeQtyNullable(request.getQty());
+		if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
+			return ErpTransferDirectResult.fail("调拨数量必须大于 0");
+		}
+
+		ErpPrdInStock instock = findInstockRow(request);
+		if (instock == null) {
+			return ErpTransferDirectResult.fail("未找到对应的生产入库分录,请刷新后重试");
+		}
+		if (!erpProperties.isAllowedTransferDirectMaterial(instock.getMaterialNumber())) {
+			return ErpTransferDirectResult.fail("物料 " + instock.getMaterialNumber() + " 不在 HEV/115 调拨范围内");
+		}
+		if (StringUtils.isBlank(instock.getLotNumber())) {
+			return ErpTransferDirectResult.fail("所选生产入库行批号为空,无法外发调拨");
+		}
+		BigDecimal instockQty = instock.getRealQty() == null ? BigDecimal.ZERO : instock.getRealQty();
+		if (qty.compareTo(instockQty) > 0) {
+			return ErpTransferDirectResult.fail("调拨数量不能超过生产入库数量 "
+					+ instockQty.stripTrailingZeros().toPlainString());
+		}
+
+		String srcStockNumber = firstNonBlank(request.getSrcStockNumber(), instock.getStockNumber(),
+				erpProperties.getTransferDirectDefaultSrcStockNumber());
+		String destStockNumber = StringUtils.trim(request.getDestStockNumber());
+		if (StringUtils.isBlank(srcStockNumber)) {
+			return ErpTransferDirectResult.fail("请选择调出仓库");
+		}
+		if (StringUtils.isBlank(destStockNumber)) {
+			return ErpTransferDirectResult.fail("请选择调入仓库");
+		}
+		if (srcStockNumber.equals(destStockNumber)) {
+			return ErpTransferDirectResult.fail("调入仓库不能与调出仓库相同");
+		}
+
+		String srcStockLocNumber = StringUtils.trim(request.getSrcStockLocNumber());
+		String destStockLocNumber = StringUtils.trim(request.getDestStockLocNumber());
+		if (erpStockLocService.requiresStockLocation(srcStockNumber) && StringUtils.isBlank(srcStockLocNumber)) {
+			return ErpTransferDirectResult.fail("调出仓库需要选择仓位");
+		}
+		if (erpStockLocService.requiresStockLocation(destStockNumber) && StringUtils.isBlank(destStockLocNumber)) {
+			return ErpTransferDirectResult.fail("调入仓库需要选择仓位");
+		}
+
+		String unitNumber = resolveUnitNumber(instock);
+		if (StringUtils.isBlank(unitNumber)) {
+			return ErpTransferDirectResult.fail("物料单位编码为空,无法调拨");
+		}
+
+		JSONObject saveData = buildSaveData(instock, qty, unitNumber, srcStockNumber, srcStockLocNumber,
+				destStockNumber, destStockLocNumber);
+		logger.info("ERP transfer direct save request: {}", saveData.toJSONString());
+		JSONObject response = erpClient.save(FORM_ID, saveData);
+		return parseSaveResponse(response);
+	}
+
+	private ErpPrdInStock findInstockRow(ErpTransferDirectSubmitRequest request) {
+		List<ErpPrdInStock> rows = erpPrdInStockService.findTransferSourceByMoBillNo(request.getMoBillNo().trim());
+		String entryId = request.getInstockEntryId().trim();
+		String billNo = StringUtils.trim(request.getInstockBillNo());
+		for (ErpPrdInStock row : rows) {
+			if (!entryId.equals(StringUtils.trim(row.getEntryId()))) {
+				continue;
+			}
+			if (StringUtils.isNotBlank(billNo) && !billNo.equals(StringUtils.trim(row.getBillNo()))) {
+				continue;
+			}
+			return row;
+		}
+		return null;
+	}
+
+	private String resolveUnitNumber(ErpPrdInStock instock) {
+		if (StringUtils.isNotBlank(instock.getUnitNumber())) {
+			return instock.getUnitNumber().trim();
+		}
+		return erpProductionMaterialService.findMaterialUnitNumber(instock.getMaterialNumber());
+	}
+
+	private JSONObject buildSaveData(ErpPrdInStock instock, BigDecimal qty, String unitNumber,
+			String srcStockNumber, String srcStockLocNumber, String destStockNumber, String destStockLocNumber) {
+		String orgNumber = firstNonBlank(instock.getPrdOrgNumber(), erpProperties.getOrgNumber(), "110");
+		String ownerType = erpProperties.getTransferDirectOwnerType();
+		String keeperType = erpProperties.getTransferDirectKeeperType();
+		String stockStatus = erpProperties.getTransferDirectStockStatus();
+		Object erpQty = ErpKingdeeParseUtils.toErpQty(qty);
+
+		JSONObject model = new JSONObject(true);
+		model.put("FID", 0);
+		model.put("FBillTypeID", numberObject(erpProperties.getTransferDirectBillType(), true));
+		model.put("FTransferBizType", erpProperties.getTransferDirectBizType());
+		model.put("FTransferDirect", erpProperties.getTransferDirectDirection());
+		model.put("FStockOrgId", numberObject(orgNumber, false));
+		model.put("FStockOutOrgId", numberObject(orgNumber, false));
+		model.put("FSaleOrgId", numberObject(orgNumber, false));
+		model.put("FSettleOrgId", numberObject(orgNumber, false));
+		model.put("FOwnerTypeIdHead", ownerType);
+		model.put("FOwnerIdHead", numberObject(orgNumber, false));
+		model.put("FOwnerTypeOutIdHead", ownerType);
+		model.put("FOwnerOutIdHead", numberObject(orgNumber, false));
+		model.put("FDate", LocalDate.now().format(DATE_FORMAT));
+		model.put("F_XYLX_BillStatus_re5", erpProperties.getTransferDirectBillSignStatus());
+		model.put("F_XYLX_Combo_83g", erpProperties.getTransferDirectCategory());
+
+		JSONObject entry = new JSONObject(true);
+		entry.put("FEntryID", 0);
+		entry.put("FMaterialId", numberObject(instock.getMaterialNumber(), false));
+		entry.put("FUnitID", numberObject(unitNumber, false));
+		entry.put("FBaseUnitId", numberObject(unitNumber, false));
+		entry.put("FQty", erpQty);
+		entry.put("FBaseQty", erpQty);
+		entry.put("FLot", numberObject(instock.getLotNumber(), false));
+		entry.put("FSrcStockId", numberObject(srcStockNumber, false));
+		entry.put("FDestStockId", numberObject(destStockNumber, false));
+		entry.put("FSrcStockStatusId", numberObject(stockStatus, false));
+		entry.put("FDestStockStatusId", numberObject(stockStatus, false));
+		entry.put("FOwnerTypeId", ownerType);
+		entry.put("FOwnerId", numberObject(orgNumber, false));
+		entry.put("FOwnerTypeOutId", ownerType);
+		entry.put("FOwnerOutId", numberObject(orgNumber, false));
+		entry.put("FKeeperTypeId", keeperType);
+		entry.put("FKeeperId", numberObject(orgNumber, false));
+		entry.put("FKeeperTypeOutId", keeperType);
+		entry.put("FKeeperOutId", numberObject(orgNumber, false));
+		putSrcStockLoc(entry, srcStockNumber, srcStockLocNumber);
+		putDestStockLoc(entry, destStockNumber, destStockLocNumber);
+
+		JSONArray entryList = new JSONArray();
+		entryList.add(entry);
+		model.put("FBillEntry", entryList);
+
+		JSONObject saveData = new JSONObject(true);
+		saveData.put("NeedUpDateFields", new JSONArray());
+		saveData.put("NeedReturnFields", JSONArray.parseArray("[\"FBillNo\"]"));
+		saveData.put("IsDeleteEntry", "true");
+		saveData.put("IsVerifyBaseDataField", "false");
+		saveData.put("IsEntryBatchFill", "true");
+		saveData.put("ValidateFlag", "true");
+		saveData.put("NumberSearch", "true");
+		saveData.put("IsAutoAdjustField", "true");
+		saveData.put("IgnoreInterationFlag", "true");
+		saveData.put("Model", model);
+		return saveData;
+	}
+
+	private void putSrcStockLoc(JSONObject entry, String stockNumber, String stockLocNumber) {
+		if (!erpStockLocService.requiresStockLocation(stockNumber)) {
+			return;
+		}
+		String locNumber = StringUtils.trim(stockLocNumber);
+		if (StringUtils.isBlank(locNumber)) {
+			return;
+		}
+		String locKey = erpStockLocService.resolveLocKeyForStock(stockNumber);
+		String saveKey = toSrcStockLocSaveKey(locKey);
+		JSONObject flexValue = numberObject(locNumber, false);
+		JSONObject stockLoc = new JSONObject(true);
+		stockLoc.put(saveKey, flexValue);
+		entry.put("FSrcStockLocId", stockLoc);
+	}
+
+	private void putDestStockLoc(JSONObject entry, String stockNumber, String stockLocNumber) {
+		if (!erpStockLocService.requiresStockLocation(stockNumber)) {
+			return;
+		}
+		String locNumber = StringUtils.trim(stockLocNumber);
+		if (StringUtils.isBlank(locNumber)) {
+			return;
+		}
+		String locKey = erpStockLocService.resolveLocKeyForStock(stockNumber);
+		String saveKey = toDestStockLocSaveKey(locKey);
+		JSONObject flexValue = numberObject(locNumber, false);
+		JSONObject stockLoc = new JSONObject(true);
+		stockLoc.put(saveKey, flexValue);
+		entry.put("FDestStockLocId", stockLoc);
+	}
+
+	private String toSrcStockLocSaveKey(String locKey) {
+		if (StringUtils.isBlank(locKey)) {
+			return "FSRCSTOCKLOCID__FF100002";
+		}
+		String key = locKey.trim();
+		if (key.startsWith("FSRCSTOCKLOCID__")) {
+			return key;
+		}
+		if (key.startsWith("F")) {
+			return "FSRCSTOCKLOCID__F" + key;
+		}
+		return "FSRCSTOCKLOCID__FF" + key;
+	}
+
+	private String toDestStockLocSaveKey(String locKey) {
+		if (StringUtils.isBlank(locKey)) {
+			return "FDESTSTOCKLOCID__FF100002";
+		}
+		String key = locKey.trim();
+		if (key.startsWith("FDESTSTOCKLOCID__")) {
+			return key;
+		}
+		if (key.startsWith("F")) {
+			return "FDESTSTOCKLOCID__F" + key;
+		}
+		return "FDESTSTOCKLOCID__FF" + key;
+	}
+
+	private JSONObject numberObject(String number, boolean useFNumberKey) {
+		JSONObject object = new JSONObject();
+		object.put(useFNumberKey ? "FNUMBER" : "FNumber", number);
+		return object;
+	}
+
+	private String firstNonBlank(String... values) {
+		for (String value : values) {
+			if (StringUtils.isNotBlank(value)) {
+				return value.trim();
+			}
+		}
+		return "";
+	}
+
+	private ErpTransferDirectResult parseSaveResponse(JSONObject response) {
+		if (response == null) {
+			return ErpTransferDirectResult.fail("ERP 返回为空");
+		}
+		JSONObject resultWrapper = response.getJSONObject("Result");
+		JSONObject status = resultWrapper == null ? null : resultWrapper.getJSONObject("ResponseStatus");
+		if (status != null && Boolean.TRUE.equals(status.getBoolean("IsSuccess"))) {
+			JSONObject result = resultWrapper.getJSONObject("Result");
+			String billNo = result == null ? "" : result.getString("Number");
+			if (StringUtils.isBlank(billNo)) {
+				billNo = resultWrapper.getString("Number");
+			}
+			String billId = result == null ? "" : result.getString("Id");
+			if (StringUtils.isBlank(billId)) {
+				billId = resultWrapper.getString("Id");
+			}
+			return ErpTransferDirectResult.success(billNo, billId);
+		}
+		return ErpTransferDirectResult.fail(extractErrorMessage(status, response));
+	}
+
+	private String extractErrorMessage(JSONObject status, JSONObject response) {
+		if (status != null) {
+			JSONArray errors = status.getJSONArray("Errors");
+			if (errors != null && !errors.isEmpty()) {
+				StringBuilder builder = new StringBuilder();
+				for (int i = 0; i < errors.size(); i++) {
+					JSONObject error = errors.getJSONObject(i);
+					if (builder.length() > 0) {
+						builder.append(";");
+					}
+					builder.append(error.getString("Message"));
+				}
+				if (builder.length() > 0) {
+					return builder.toString();
+				}
+			}
+		}
+		return "ERP 直接调拨单保存失败:" + response.toJSONString();
+	}
+}

+ 331 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectService.java

@@ -0,0 +1,331 @@
+package com.jeesite.modules.mes.erp;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.jeesite.common.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ERP 直接调拨单 BillQuery(STK_TransferDirect)。
+ * 固定仅查 HEV / 115 两种物料编码。
+ */
+@Service
+public class ErpTransferDirectService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpTransferDirectService.class);
+	private static final String FORM_ID = "STK_TransferDirect";
+	private static final String BILL_TYPE_FIELD = "FBillTypeID.FNUMBER";
+	private static final String MATERIAL_NUMBER_FIELD = "FMaterialId.FNUMBER";
+	private static final String DETAIL_FIELD_KEYS = String.join(",",
+			"FID",
+			"FBILLNO",
+			"FDate",
+			"FDocumentStatus",
+			BILL_TYPE_FIELD,
+			"FBillTypeID.FNAME",
+			"FStockOrgId.FNUMBER",
+			"FStockOrgId.FNAME",
+			"FStockOutOrgId.FNUMBER",
+			"FStockOutOrgId.FNAME",
+			"FBillEntry_FEntryID",
+			"FBillEntry_FSeq",
+			MATERIAL_NUMBER_FIELD,
+			"FMaterialName",
+			"FUnitID.FNAME",
+			"FQty",
+			"FSrcStockId.FNUMBER",
+			"FSrcStockId.FNAME",
+			"FDestStockId.FNUMBER",
+			"FDestStockId.FNAME",
+			"FLot.FNUMBER",
+			"FSrcBillNo",
+			"FOrderNo",
+			"FNote");
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+
+	public ErpTransferDirectService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+	}
+
+	/**
+	 * 分页查询调拨明细分录(仅 HEV / 115 物料)。
+	 */
+	public List<ErpTransferDirect> findPageList(ErpTransferDirect query, int startRow, int limit) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", DETAIL_FIELD_KEYS);
+		data.put("FilterString", buildFilterString(query));
+		data.put("OrderString", "FDate DESC,FBILLNO DESC,FBillEntry_FSeq");
+		data.put("StartRow", Math.max(startRow, 0));
+		data.put("Limit", Math.max(limit, 1));
+		logger.info("ERP transfer direct query: {}", data.toJSONString());
+		return mapRows(erpClient.billQuery(data));
+	}
+
+	/**
+	 * 指定调拨单号的全部分录(仍限定 HEV / 115 物料)。
+	 */
+	public List<ErpTransferDirect> findDetailList(String billNo) {
+		if (StringUtils.isBlank(billNo)) {
+			return new ArrayList<>();
+		}
+		ErpTransferDirect query = new ErpTransferDirect();
+		query.setBillNoQuery(billNo.trim());
+		return findPageList(query, 0, 2000);
+	}
+
+	/**
+	 * 生产订单下查:优先按源单/订单号/关联内码,再按生产入库批号关联调拨单。
+	 * ERP 直接调拨单无 FMoBillNo,与生产订单的关联主要通过生产入库批号(FLot)建立。
+	 */
+	public List<ErpTransferDirect> findTraceListByMoBillNo(String moBillNo, String moFid, List<String> instockLots) {
+		if (StringUtils.isBlank(moBillNo)) {
+			return new ArrayList<>();
+		}
+		Map<String, ErpTransferDirect> merged = new LinkedHashMap<>();
+		appendTraceRows(merged, findTraceByDirectLink(moBillNo.trim(), moFid));
+		appendTraceRows(merged, findTraceByInstockLots(instockLots));
+		return new ArrayList<>(merged.values());
+	}
+
+	private void appendTraceRows(Map<String, ErpTransferDirect> merged, List<ErpTransferDirect> rows) {
+		if (rows == null || rows.isEmpty()) {
+			return;
+		}
+		for (ErpTransferDirect row : rows) {
+			if (row == null || StringUtils.isBlank(row.getId())) {
+				continue;
+			}
+			merged.putIfAbsent(row.getId(), row);
+		}
+	}
+
+	private List<ErpTransferDirect> findTraceByDirectLink(String moBillNo, String moFid) {
+		String escapedMo = escape(moBillNo);
+		StringBuilder linkFilter = new StringBuilder("(FSrcBillNo='").append(escapedMo).append("' OR FOrderNo='")
+				.append(escapedMo).append("')");
+		if (StringUtils.isNotBlank(moFid)) {
+			linkFilter.append(" OR FBillEntry_Link_FSBillId=").append(moFid.trim());
+		}
+		return queryTraceList("(" + linkFilter + ")");
+	}
+
+	private List<ErpTransferDirect> findTraceByInstockLots(List<String> instockLots) {
+		if (instockLots == null || instockLots.isEmpty()) {
+			return new ArrayList<>();
+		}
+		List<String> normalizedLots = new ArrayList<>();
+		for (String lot : instockLots) {
+			if (StringUtils.isBlank(lot)) {
+				continue;
+			}
+			String trimmed = lot.trim();
+			if (!normalizedLots.contains(trimmed)) {
+				normalizedLots.add(trimmed);
+			}
+		}
+		if (normalizedLots.isEmpty()) {
+			return new ArrayList<>();
+		}
+		StringBuilder builder = new StringBuilder("FLot.FNUMBER IN(");
+		for (int i = 0; i < normalizedLots.size(); i++) {
+			if (i > 0) {
+				builder.append(",");
+			}
+			builder.append("'").append(escape(normalizedLots.get(i))).append("'");
+		}
+		builder.append(")");
+		return queryTraceList(builder.toString());
+	}
+
+	private List<ErpTransferDirect> queryTraceList(String traceFilter) {
+		if (StringUtils.isBlank(traceFilter)) {
+			return new ArrayList<>();
+		}
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", DETAIL_FIELD_KEYS);
+		data.put("FilterString", appendAnd(buildTraceBaseFilter(), traceFilter));
+		data.put("OrderString", "FDate DESC,FBILLNO DESC,FBillEntry_FSeq");
+		data.put("StartRow", 0);
+		data.put("Limit", 2000);
+		logger.info("ERP transfer direct trace query: {}", data.toJSONString());
+		return mapRows(erpClient.billQuery(data));
+	}
+
+	private String buildTraceBaseFilter() {
+		return buildBaseFilter();
+	}
+
+	private List<ErpTransferDirect> mapRows(JSONArray rows) {
+		List<ErpTransferDirect> list = new ArrayList<>();
+		for (int i = 0; i < rows.size(); i++) {
+			list.add(toDetail(rows.getJSONObject(i), i));
+		}
+		return list;
+	}
+
+	private ErpTransferDirect toDetail(JSONObject row, int index) {
+		ErpTransferDirect item = new ErpTransferDirect();
+		item.setFid(readString(row, "FID"));
+		item.setBillNo(readString(row, "FBILLNO", "FBillNo"));
+		item.setEntryId(readString(row, "FBillEntry_FEntryID", "FBillEntry_FEntryId"));
+		item.setSeq(readString(row, "FBillEntry_FSeq"));
+		String rowId = item.getBillNo();
+		if (StringUtils.isNotBlank(item.getEntryId())) {
+			rowId = rowId + "-" + item.getEntryId();
+		} else if (index >= 0) {
+			rowId = rowId + "-" + index;
+		}
+		item.setId(rowId);
+		item.setDocumentStatus(readString(row, "FDocumentStatus"));
+		item.setDocumentStatusName(documentStatusName(item.getDocumentStatus()));
+		item.setDate(formatErpDate(readString(row, "FDate")));
+		item.setBillTypeNumber(readString(row, BILL_TYPE_FIELD, "FBillTypeID.FNumber"));
+		item.setBillTypeName(readString(row, "FBillTypeID.FNAME", "FBillTypeID.FName"));
+		item.setStockOrgNumber(readString(row, "FStockOrgId.FNUMBER", "FStockOrgId.FNumber"));
+		item.setStockOrgName(readString(row, "FStockOrgId.FNAME", "FStockOrgId.FName"));
+		item.setStockOutOrgNumber(readString(row, "FStockOutOrgId.FNUMBER", "FStockOutOrgId.FNumber"));
+		item.setStockOutOrgName(readString(row, "FStockOutOrgId.FNAME", "FStockOutOrgId.FName"));
+		item.setMaterialNumber(readString(row, MATERIAL_NUMBER_FIELD, "FMaterialId.FNumber"));
+		item.setMaterialName(readString(row, "FMaterialName", "FMaterialId.FNAME", "FMaterialId.FName"));
+		item.setUnitName(readString(row, "FUnitID.FNAME", "FUnitId.FNAME", "FUnitId.FName"));
+		item.setQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FQty")));
+		item.setSrcStockNumber(readString(row, "FSrcStockId.FNUMBER", "FSrcStockId.FNumber"));
+		item.setSrcStockName(readString(row, "FSrcStockId.FNAME", "FSrcStockId.FName"));
+		item.setDestStockNumber(readString(row, "FDestStockId.FNUMBER", "FDestStockId.FNumber"));
+		item.setDestStockName(readString(row, "FDestStockId.FNAME", "FDestStockId.FName"));
+		item.setLotNumber(readString(row, "FLot.FNUMBER", "FLot.FNumber"));
+		item.setSrcBillNo(trimToEmpty(readString(row, "FSrcBillNo")));
+		item.setOrderNo(trimToEmpty(readString(row, "FOrderNo")));
+		item.setNote(readString(row, "FNote"));
+		return item;
+	}
+
+	private String trimToEmpty(String value) {
+		return value == null ? "" : value.trim();
+	}
+
+	private String buildFilterString(ErpTransferDirect query) {
+		String filter = buildBaseFilter();
+		if (query == null) {
+			return filter;
+		}
+		if (StringUtils.isNotBlank(query.getBillNoQuery())) {
+			filter = appendAnd(filter, "FBILLNO like '%" + escape(query.getBillNoQuery()) + "%'");
+		} else if (StringUtils.isNotBlank(query.getBillNo())) {
+			filter = appendAnd(filter, "FBILLNO='" + escape(query.getBillNo()) + "'");
+		}
+		if (StringUtils.isNotBlank(query.getDocumentStatusQuery())) {
+			filter = appendAnd(filter, "FDocumentStatus='" + escape(query.getDocumentStatusQuery()) + "'");
+		}
+		String materialFilter = buildMaterialFilter(query.getMaterialNumberQuery());
+		if (StringUtils.isNotBlank(materialFilter)) {
+			filter = appendAnd(filter, materialFilter);
+		}
+		return filter;
+	}
+
+	/**
+	 * 物料范围:默认固定 HEV + 115;页面可选缩小到其中一个。
+	 */
+	private String buildMaterialFilter(String materialNumberQuery) {
+		if (StringUtils.isNotBlank(materialNumberQuery)) {
+			String trimmed = materialNumberQuery.trim();
+			if (erpProperties.isAllowedTransferDirectMaterial(trimmed)) {
+				return MATERIAL_NUMBER_FIELD + "='" + escape(trimmed) + "'";
+			}
+		}
+		return buildAllowedMaterialInFilter();
+	}
+
+	private String buildAllowedMaterialInFilter() {
+		List<String> materials = erpProperties.getTransferDirectMaterialNumbers();
+		if (materials == null || materials.isEmpty()) {
+			return MATERIAL_NUMBER_FIELD + "='8.10.HEV.10.047696'";
+		}
+		StringBuilder builder = new StringBuilder(MATERIAL_NUMBER_FIELD + " IN(");
+		for (int i = 0; i < materials.size(); i++) {
+			if (i > 0) {
+				builder.append(",");
+			}
+			builder.append("'").append(escape(materials.get(i))).append("'");
+		}
+		builder.append(")");
+		return builder.toString();
+	}
+
+	private String buildBaseFilter() {
+		String filter = "";
+		if (StringUtils.isNotBlank(erpProperties.getTransferDirectBillType())) {
+			filter = BILL_TYPE_FIELD + "='" + escape(erpProperties.getTransferDirectBillType()) + "'";
+		}
+		return appendAnd(filter, buildStockOrgFilter());
+	}
+
+	private String buildStockOrgFilter() {
+		if (!erpProperties.isFilterCurrentOrg()) {
+			return "";
+		}
+		if (StringUtils.isNotBlank(erpProperties.getOrgNumber())) {
+			return "FStockOrgId.FNUMBER='" + escape(erpProperties.getOrgNumber()) + "'";
+		}
+		Long currentOrganizationId = erpClient.getCurrentOrganizationId();
+		if (currentOrganizationId == null) {
+			return "1=1";
+		}
+		return "FStockOrgId=" + currentOrganizationId;
+	}
+
+	private String appendAnd(String left, String right) {
+		if (StringUtils.isBlank(left)) {
+			return right;
+		}
+		if (StringUtils.isBlank(right)) {
+			return left;
+		}
+		return left + " AND " + right;
+	}
+
+	private String escape(String value) {
+		return value == null ? "" : value.trim().replace("'", "''");
+	}
+
+	private String readString(JSONObject row, String... keys) {
+		return ErpKingdeeParseUtils.readString(row, keys);
+	}
+
+	private String formatErpDate(String rawValue) {
+		if (StringUtils.isBlank(rawValue)) {
+			return "";
+		}
+		String trimmed = rawValue.trim();
+		return trimmed.length() >= 10 ? trimmed.substring(0, 10) : trimmed;
+	}
+
+	private String documentStatusName(String status) {
+		if ("A".equals(status)) {
+			return "创建";
+		}
+		if ("B".equals(status)) {
+			return "审核中";
+		}
+		if ("C".equals(status)) {
+			return "已审核";
+		}
+		if ("D".equals(status)) {
+			return "重新审核";
+		}
+		return status;
+	}
+}

+ 93 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpTransferDirectSubmitRequest.java

@@ -0,0 +1,93 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 提交 ERP 直接调拨单请求
+ */
+public class ErpTransferDirectSubmitRequest implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/** 生产订单号 */
+	private String moBillNo;
+	/** 生产入库分录内码 */
+	private String instockEntryId;
+	/** 生产入库单号(校验用) */
+	private String instockBillNo;
+	/** 调拨数量 */
+	private BigDecimal qty;
+	/** 调出仓库编码 */
+	private String srcStockNumber;
+	/** 调出仓位编码 */
+	private String srcStockLocNumber;
+	/** 调入仓库编码 */
+	private String destStockNumber;
+	/** 调入仓位编码 */
+	private String destStockLocNumber;
+
+	public String getMoBillNo() {
+		return moBillNo;
+	}
+
+	public void setMoBillNo(String moBillNo) {
+		this.moBillNo = moBillNo;
+	}
+
+	public String getInstockEntryId() {
+		return instockEntryId;
+	}
+
+	public void setInstockEntryId(String instockEntryId) {
+		this.instockEntryId = instockEntryId;
+	}
+
+	public String getInstockBillNo() {
+		return instockBillNo;
+	}
+
+	public void setInstockBillNo(String instockBillNo) {
+		this.instockBillNo = instockBillNo;
+	}
+
+	public BigDecimal getQty() {
+		return qty;
+	}
+
+	public void setQty(BigDecimal qty) {
+		this.qty = qty;
+	}
+
+	public String getSrcStockNumber() {
+		return srcStockNumber;
+	}
+
+	public void setSrcStockNumber(String srcStockNumber) {
+		this.srcStockNumber = srcStockNumber;
+	}
+
+	public String getSrcStockLocNumber() {
+		return srcStockLocNumber;
+	}
+
+	public void setSrcStockLocNumber(String srcStockLocNumber) {
+		this.srcStockLocNumber = srcStockLocNumber;
+	}
+
+	public String getDestStockNumber() {
+		return destStockNumber;
+	}
+
+	public void setDestStockNumber(String destStockNumber) {
+		this.destStockNumber = destStockNumber;
+	}
+
+	public String getDestStockLocNumber() {
+		return destStockLocNumber;
+	}
+
+	public void setDestStockLocNumber(String destStockLocNumber) {
+		this.destStockLocNumber = destStockLocNumber;
+	}
+}

+ 169 - 0
src/main/java/com/jeesite/modules/mes/erp/KingdeeErpProperties.java

@@ -106,6 +106,64 @@ public class KingdeeErpProperties {
 	@Value("${erp.k3cloud.in-stock.stock-loc-number:}")
 	private String inStockStockLocNumber;
 
+	/** 直接调拨单 STK_TransferDirect 单据类型 */
+	@Value("${erp.k3cloud.transfer-direct.bill-type:ZJDB01_SYS}")
+	private String transferDirectBillType;
+
+	/** 直接调拨单固定物料范围(HEV / 115) */
+	@Value("${erp.k3cloud.transfer-direct.material-numbers:8.10.HEV.10.047696,8.35.115.10.5003146600021}")
+	private String transferDirectMaterialNumbers;
+
+	@Value("${erp.k3cloud.transfer-direct.transfer-biz-type:InnerOrgTransfer}")
+	private String transferDirectBizType;
+
+	@Value("${erp.k3cloud.transfer-direct.transfer-direct-type:GENERAL}")
+	private String transferDirectDirection;
+
+	/** 调拨类别:3=外发调拨 */
+	@Value("${erp.k3cloud.transfer-direct.transfer-category:3}")
+	private String transferDirectCategory;
+
+	@Value("${erp.k3cloud.transfer-direct.bill-sign-status:A}")
+	private String transferDirectBillSignStatus;
+
+	@Value("${erp.k3cloud.transfer-direct.stock-status:KCZT01_SYS}")
+	private String transferDirectStockStatus;
+
+	@Value("${erp.k3cloud.transfer-direct.owner-type:BD_OwnerOrg}")
+	private String transferDirectOwnerType;
+
+	@Value("${erp.k3cloud.transfer-direct.keeper-type:BD_KeeperOrg}")
+	private String transferDirectKeeperType;
+
+	@Value("${erp.k3cloud.transfer-direct.default-src-stock-number:}")
+	private String transferDirectDefaultSrcStockNumber;
+
+	@Value("${erp.k3cloud.transfer-direct.default-dest-stock-number:}")
+	private String transferDirectDefaultDestStockNumber;
+
+	/** 生产入库单 PRD_INSTOCK 固定物料范围(HEV / 115,与直接调拨单一致) */
+	@Value("${erp.k3cloud.prd-in-stock.material-numbers:8.10.HEV.10.047696,8.35.115.10.5003146600021}")
+	private String prdInStockMaterialNumbers;
+
+	@Value("${erp.k3cloud.prd-in-stock.bill-type:SCRKD01_SYS}")
+	private String prdInStockBillType;
+
+	@Value("${erp.k3cloud.prd-in-stock.in-stock-type:1}")
+	private String prdInStockInStockType;
+
+	@Value("${erp.k3cloud.prd-in-stock.stock-status:KCZT01_SYS}")
+	private String prdInStockStockStatus;
+
+	@Value("${erp.k3cloud.prd-in-stock.owner-type:BD_OwnerOrg}")
+	private String prdInStockOwnerType;
+
+	@Value("${erp.k3cloud.prd-in-stock.keeper-type:BD_KeeperOrg}")
+	private String prdInStockKeeperType;
+
+	@Value("${erp.k3cloud.prd-in-stock.default-stock-number:}")
+	private String prdInStockDefaultStockNumber;
+
 	public String getBaseUrl() {
 		if (baseUrl.endsWith("/")) {
 			return baseUrl.substring(0, baseUrl.length() - 1);
@@ -233,6 +291,117 @@ public class KingdeeErpProperties {
 		return inStockStockLocNumber;
 	}
 
+	public String getTransferDirectBillType() {
+		return transferDirectBillType;
+	}
+
+	public java.util.List<String> getTransferDirectMaterialNumbers() {
+		return parseMaterialNumbers(transferDirectMaterialNumbers);
+	}
+
+	public boolean isAllowedTransferDirectMaterial(String materialNumber) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return false;
+		}
+		String trimmed = materialNumber.trim();
+		for (String allowed : getTransferDirectMaterialNumbers()) {
+			if (trimmed.equals(allowed)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public String getTransferDirectBizType() {
+		return transferDirectBizType;
+	}
+
+	public String getTransferDirectDirection() {
+		return transferDirectDirection;
+	}
+
+	public String getTransferDirectCategory() {
+		return transferDirectCategory;
+	}
+
+	public String getTransferDirectBillSignStatus() {
+		return transferDirectBillSignStatus;
+	}
+
+	public String getTransferDirectStockStatus() {
+		return transferDirectStockStatus;
+	}
+
+	public String getTransferDirectOwnerType() {
+		return transferDirectOwnerType;
+	}
+
+	public String getTransferDirectKeeperType() {
+		return transferDirectKeeperType;
+	}
+
+	public String getTransferDirectDefaultSrcStockNumber() {
+		return transferDirectDefaultSrcStockNumber;
+	}
+
+	public String getTransferDirectDefaultDestStockNumber() {
+		return transferDirectDefaultDestStockNumber;
+	}
+
+	public java.util.List<String> getPrdInStockMaterialNumbers() {
+		return parseMaterialNumbers(prdInStockMaterialNumbers);
+	}
+
+	public boolean isAllowedPrdInStockMaterial(String materialNumber) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return false;
+		}
+		String trimmed = materialNumber.trim();
+		for (String allowed : getPrdInStockMaterialNumbers()) {
+			if (trimmed.equals(allowed)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public String getPrdInStockBillType() {
+		return prdInStockBillType;
+	}
+
+	public String getPrdInStockInStockType() {
+		return prdInStockInStockType;
+	}
+
+	public String getPrdInStockStockStatus() {
+		return prdInStockStockStatus;
+	}
+
+	public String getPrdInStockOwnerType() {
+		return prdInStockOwnerType;
+	}
+
+	public String getPrdInStockKeeperType() {
+		return prdInStockKeeperType;
+	}
+
+	public String getPrdInStockDefaultStockNumber() {
+		return prdInStockDefaultStockNumber;
+	}
+
+	private java.util.List<String> parseMaterialNumbers(String materialNumbers) {
+		java.util.List<String> list = new java.util.ArrayList<>();
+		if (StringUtils.isBlank(materialNumbers)) {
+			return list;
+		}
+		for (String part : materialNumbers.split(",")) {
+			if (StringUtils.isNotBlank(part)) {
+				list.add(part.trim());
+			}
+		}
+		return list;
+	}
+
 	/**
 	 * 判断所选仓库是否需要填写仓位(当前按配置仓库 + 仓位维度键判断)。
 	 */

+ 68 - 0
src/main/java/com/jeesite/modules/mes/erp/MesErpLineMaterialMapping.java

@@ -0,0 +1,68 @@
+package com.jeesite.modules.mes.erp;
+
+import com.jeesite.common.lang.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * MES 产线与 ERP 物料编码映射(生产入库选批次用)。
+ * HEVXT → HEV;115XT → 115(与 MesErpScheduleService、产线设备配置一致)。
+ */
+public final class MesErpLineMaterialMapping {
+
+	private static final String HEV_MATERIAL = "8.10.HEV.10.047696";
+	private static final String MATERIAL_115 = "8.35.115.10.5003146600021";
+
+	private static final Map<String, String> LINE_TO_MATERIAL;
+
+	static {
+		Map<String, String> map = new LinkedHashMap<>();
+		map.put("HEVXT", HEV_MATERIAL);
+		map.put("115XT", MATERIAL_115);
+		LINE_TO_MATERIAL = Collections.unmodifiableMap(map);
+	}
+
+	private MesErpLineMaterialMapping() {
+	}
+
+	public static String resolveMaterialByLineSn(String lineSn) {
+		if (StringUtils.isBlank(lineSn)) {
+			return "";
+		}
+		String key = lineSn.trim().toUpperCase();
+		return LINE_TO_MATERIAL.getOrDefault(key, "");
+	}
+
+	public static List<String> resolveLineSnList(String materialNumber) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return new ArrayList<>();
+		}
+		String trimmed = materialNumber.trim();
+		List<String> lines = new ArrayList<>();
+		for (Map.Entry<String, String> entry : LINE_TO_MATERIAL.entrySet()) {
+			if (trimmed.equals(entry.getValue())) {
+				lines.add(entry.getKey());
+			}
+		}
+		return lines;
+	}
+
+	public static boolean isLineAllowedForMaterial(String lineSn, String materialNumber) {
+		if (StringUtils.isBlank(lineSn) || StringUtils.isBlank(materialNumber)) {
+			return false;
+		}
+		return materialNumber.trim().equals(resolveMaterialByLineSn(lineSn));
+	}
+
+	public static List<String> allLineSnList() {
+		return new ArrayList<>(LINE_TO_MATERIAL.keySet());
+	}
+
+	public static Map<String, String> lineMaterialMap() {
+		return LINE_TO_MATERIAL;
+	}
+}

+ 58 - 0
src/main/java/com/jeesite/modules/mes/service/MesBatchSnService.java

@@ -2,6 +2,7 @@ package com.jeesite.modules.mes.service;
 
 import java.util.List;
 
+import com.jeesite.modules.mes.erp.MesErpLineMaterialMapping;
 import com.jeesite.modules.mes.dao.MesDeviceDao;
 import com.jeesite.modules.mes.entity.MesDevice;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -13,6 +14,9 @@ import com.jeesite.common.service.CrudService;
 import com.jeesite.modules.mes.entity.MesBatchSn;
 import com.jeesite.modules.mes.dao.MesBatchSnDao;
 
+import java.util.ArrayList;
+import java.util.Date;
+
 /**
  * 批次码记录表Service
  * @author mes
@@ -29,6 +33,60 @@ public class MesBatchSnService extends CrudService<MesBatchSnDao, MesBatchSn> {
 	public MesBatchSn findInfo(MesBatchSn mesBatchSn){
 		return mesBatchSnDao.findInfo(mesBatchSn);
 	}
+
+	/**
+	 * 按物料编码查询待 ERP 入库的 MES 批次码(nums&gt;0 且未入库)。
+	 */
+	public List<MesBatchSn> findPendingInstockByMaterial(String materialNumber) {
+		List<String> lineSnList = MesErpLineMaterialMapping.resolveLineSnList(materialNumber);
+		if (lineSnList.isEmpty()) {
+			return new ArrayList<>();
+		}
+		return mesBatchSnDao.findPendingInstockList(lineSnList);
+	}
+
+	/**
+	 * 按主键批量加载批次码。
+	 */
+	public List<MesBatchSn> findByIds(List<String> ids) {
+		List<MesBatchSn> list = new ArrayList<>();
+		if (ids == null) {
+			return list;
+		}
+		for (String id : ids) {
+			if (id == null || id.trim().isEmpty()) {
+				continue;
+			}
+			MesBatchSn item = super.get(id.trim());
+			if (item != null) {
+				list.add(item);
+			}
+		}
+		return list;
+	}
+
+	/**
+	 * ERP 入库成功后回写批次码状态。
+	 */
+	@Transactional
+	public void markInstocked(List<MesBatchSn> batches, String moBillNo, String moEntryId,
+			String erpBillNo, String erpLotNumber, Date instockDate) {
+		if (batches == null) {
+			return;
+		}
+		for (MesBatchSn batch : batches) {
+			if (batch == null) {
+				continue;
+			}
+			batch.setInstockStatus("1");
+			batch.setMoBillNo(moBillNo);
+			batch.setMoEntryId(moEntryId);
+			batch.setErpInstockBillNo(erpBillNo);
+			batch.setErpLotNumber(erpLotNumber);
+			batch.setErpInstockDate(instockDate);
+			super.save(batch);
+		}
+	}
 	/**
 	 * 获取单条数据
 	 * @param mesBatchSn

+ 19 - 0
src/main/java/com/jeesite/modules/mes/service/MesErpPrdInstockLogService.java

@@ -0,0 +1,19 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesErpPrdInstockLogDao;
+import com.jeesite.modules.mes.entity.MesErpPrdInstockLog;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * MES 推 ERP 生产入库日志 Service
+ */
+@Service
+public class MesErpPrdInstockLogService extends CrudService<MesErpPrdInstockLogDao, MesErpPrdInstockLog> {
+
+	@Transactional
+	public void saveLog(MesErpPrdInstockLog log) {
+		super.save(log);
+	}
+}

+ 426 - 0
src/main/java/com/jeesite/modules/mes/web/ErpPrdInStockController.java

@@ -0,0 +1,426 @@
+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.MesBatchSn;
+
+import com.jeesite.modules.mes.erp.ErpPrdInStock;
+
+import com.jeesite.modules.mes.erp.ErpPrdInStockResult;
+
+import com.jeesite.modules.mes.erp.ErpPrdInStockSaveService;
+
+import com.jeesite.modules.mes.erp.ErpPrdInStockService;
+
+import com.jeesite.modules.mes.erp.ErpPrdInStockSubmitRequest;
+
+import com.jeesite.modules.mes.erp.ErpProductionOrder;
+
+import com.jeesite.modules.mes.erp.ErpProductionOrderService;
+
+import com.jeesite.modules.mes.erp.ErpStockLocOption;
+
+import com.jeesite.modules.mes.erp.ErpStockLocService;
+
+import com.jeesite.modules.mes.erp.ErpStockOption;
+
+import com.jeesite.modules.mes.erp.ErpStockService;
+
+import com.jeesite.modules.mes.erp.KingdeeErpProperties;
+
+import com.jeesite.modules.mes.erp.MesErpLineMaterialMapping;
+
+import com.jeesite.modules.mes.service.MesBatchSnService;
+
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+
+import org.springframework.stereotype.Controller;
+
+import org.springframework.ui.Model;
+
+import org.springframework.web.bind.annotation.GetMapping;
+
+import org.springframework.web.bind.annotation.PostMapping;
+
+import org.springframework.web.bind.annotation.RequestBody;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.web.bind.annotation.ResponseBody;
+
+
+
+import javax.servlet.http.HttpServletRequest;
+
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.HashMap;
+
+import java.util.List;
+
+import java.util.Map;
+
+
+
+/**
+
+ * ERP 生产入库单查询与新增(MES 批次码驱动数量)。
+
+ */
+
+@Controller
+
+@RequestMapping(value = "${adminPath}/mes/erpPrdInStock")
+
+public class ErpPrdInStockController extends BaseController {
+
+
+
+	private final ErpPrdInStockService erpPrdInStockService;
+
+	private final ErpPrdInStockSaveService erpPrdInStockSaveService;
+
+	private final ErpProductionOrderService erpProductionOrderService;
+
+	private final MesBatchSnService mesBatchSnService;
+
+	private final ErpStockService erpStockService;
+
+	private final ErpStockLocService erpStockLocService;
+
+	private final KingdeeErpProperties erpProperties;
+
+
+
+	public ErpPrdInStockController(ErpPrdInStockService erpPrdInStockService,
+
+			ErpPrdInStockSaveService erpPrdInStockSaveService,
+
+			ErpProductionOrderService erpProductionOrderService,
+
+			MesBatchSnService mesBatchSnService,
+
+			ErpStockService erpStockService,
+
+			ErpStockLocService erpStockLocService,
+
+			KingdeeErpProperties erpProperties) {
+
+		this.erpPrdInStockService = erpPrdInStockService;
+
+		this.erpPrdInStockSaveService = erpPrdInStockSaveService;
+
+		this.erpProductionOrderService = erpProductionOrderService;
+
+		this.mesBatchSnService = mesBatchSnService;
+
+		this.erpStockService = erpStockService;
+
+		this.erpStockLocService = erpStockLocService;
+
+		this.erpProperties = erpProperties;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = {"list", ""})
+
+	public String list(ErpPrdInStock erpPrdInStock, Model model) {
+
+		model.addAttribute("erpPrdInStock", erpPrdInStock);
+
+		return "modules/mes/erpPrdInStockList";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "listData")
+
+	@ResponseBody
+
+	public Page<ErpPrdInStock> listData(ErpPrdInStock erpPrdInStock,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpPrdInStock> page = new Page<>(request, response);
+
+		int pageSize = page.getPageSize() > 0 ? page.getPageSize() : 20;
+
+		int pageNo = page.getPageNo() > 0 ? page.getPageNo() : 1;
+
+		int startRow = (pageNo - 1) * pageSize;
+
+		List<ErpPrdInStock> list = erpPrdInStockService.findPageList(erpPrdInStock, startRow, pageSize);
+
+		page.setList(list);
+
+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "detail")
+
+	public String detail(ErpPrdInStock erpPrdInStock, Model model) {
+
+		model.addAttribute("erpPrdInStock", erpPrdInStock);
+
+		return "modules/mes/erpPrdInStockDetail";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "detailData")
+
+	@ResponseBody
+
+	public Page<ErpPrdInStock> detailData(ErpPrdInStock erpPrdInStock,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpPrdInStock> page = new Page<>(request, response);
+
+		List<ErpPrdInStock> list = erpPrdInStockService.findDetailList(erpPrdInStock.getBillNo());
+
+		page.setList(list);
+
+		page.setCount(list.size());
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "selectOrder")
+
+	public String selectOrder(ErpProductionOrder erpProductionOrder, Model model) {
+
+		model.addAttribute("erpProductionOrder", erpProductionOrder);
+
+		return "modules/mes/erpPrdInStockSelectOrder";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "selectOrderData")
+
+	@ResponseBody
+
+	public Page<ErpProductionOrder> selectOrderData(ErpProductionOrder erpProductionOrder,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpProductionOrder> page = new Page<>(request, response);
+
+		int pageSize = page.getPageSize() > 0 ? page.getPageSize() : 20;
+
+		int pageNo = page.getPageNo() > 0 ? page.getPageNo() : 1;
+
+		int startRow = (pageNo - 1) * pageSize;
+
+		List<ErpProductionOrder> list = erpProductionOrderService.findPrdInStockOrderPageList(
+
+				erpProductionOrder.getBillNo(), startRow, pageSize);
+
+		page.setList(list);
+
+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "selectDetail")
+
+	public String selectDetail(ErpProductionOrder erpProductionOrder, Model model) {
+
+		model.addAttribute("erpProductionOrder", erpProductionOrder);
+
+		return "modules/mes/erpPrdInStockSelectDetail";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "selectDetailData")
+
+	@ResponseBody
+
+	public Page<ErpProductionOrder> selectDetailData(ErpProductionOrder erpProductionOrder,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpProductionOrder> page = new Page<>(request, response);
+
+		List<ErpProductionOrder> list = erpProductionOrderService.findDetailList(erpProductionOrder.getBillNo());
+
+		// 仅保留 HEV/115 物料分录
+
+		list.removeIf(row -> row == null || !erpProperties.isAllowedPrdInStockMaterial(row.getMaterialNumber()));
+
+		erpPrdInStockService.enrichMoInstockProgress(erpProductionOrder.getBillNo(), list);
+
+		page.setList(list);
+
+		page.setCount(list.size());
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@RequestMapping(value = "form")
+
+	public String form(String moBillNo, String entryId, Model model) {
+
+		String billNo = StringUtils.trim(moBillNo);
+
+		String moEntryId = StringUtils.trim(entryId);
+
+		ErpProductionOrder moEntry = erpProductionOrderService.findInstockMoEntry(billNo, moEntryId);
+
+		if (moEntry != null) {
+
+			erpPrdInStockService.enrichMoInstockProgress(billNo, java.util.Collections.singletonList(moEntry));
+
+		}
+
+		List<MesBatchSn> batchList = moEntry == null ? java.util.Collections.emptyList()
+
+				: mesBatchSnService.findPendingInstockByMaterial(moEntry.getMaterialNumber());
+
+		List<ErpStockOption> stockList = erpStockService.findUsableStockList();
+
+		model.addAttribute("moBillNo", billNo);
+
+		model.addAttribute("entryId", moEntryId);
+
+		model.addAttribute("moEntry", moEntry);
+
+		model.addAttribute("batchList", batchList);
+
+		model.addAttribute("stockList", stockList);
+
+		model.addAttribute("defaultStockNumber", erpStockService.resolveDefaultStockNumber(stockList));
+
+		if (moEntry == null) {
+
+			model.addAttribute("message", "未找到生产订单分录");
+
+		} else if (batchList.isEmpty()) {
+
+			List<String> lines = MesErpLineMaterialMapping.resolveLineSnList(moEntry.getMaterialNumber());
+
+			String lineHint = lines.isEmpty() ? "(该物料未配置产线映射)" : lines.toString();
+
+			model.addAttribute("message", "暂无待入库 MES 批次码。须同时满足:产线 " + lineHint
+
+					+ "、已绑定数量 nums>0、且未入库。"
+
+					+ " 若批次码记录表有条目但 nums=0,需产线扫码绑定工件后再入库;"
+
+					+ " 产线编号应为 HEVXT 或 115XT。");
+
+		}
+
+		return "modules/mes/erpPrdInStockForm";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@GetMapping(value = "stockLocOptions")
+
+	@ResponseBody
+
+	public Map<String, Object> stockLocOptions(String stockNumber) {
+
+		List<ErpStockLocOption> locations = erpStockLocService.findLocationsByStock(stockNumber);
+
+		Map<String, Object> result = new HashMap<>(4);
+
+		result.put("required", !locations.isEmpty());
+
+		result.put("locations", locations);
+
+		result.put("defaultLocNumber", erpStockLocService.resolveDefaultLocNumber(stockNumber, locations));
+
+		result.put("locHint", locations.isEmpty() ? "该仓库未启用仓位" : "共 " + locations.size() + " 个仓位");
+
+		return result;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpPrdInStock:view")
+
+	@PostMapping(value = "submit")
+
+	@ResponseBody
+
+	public String submit(@RequestBody ErpPrdInStockSubmitRequest request) {
+
+		ErpPrdInStockResult result = erpPrdInStockSaveService.submit(request);
+
+		if (result.isSuccess()) {
+
+			String lotMsg = StringUtils.isNotBlank(result.getLotNumber())
+
+					? ",ERP批号:" + result.getLotNumber() : "";
+
+			return renderResult(Global.TRUE, text("生产入库单保存成功,单号:{0};合计数量 {1},绑定批次 {2} 个{3}",
+
+					result.getBillNo(),
+
+					result.getTotalQty() == null ? "0" : result.getTotalQty().stripTrailingZeros().toPlainString(),
+
+					String.valueOf(result.getBatchCount()), lotMsg));
+
+		}
+
+		return renderResult(Global.FALSE, result.getMessage());
+
+	}
+
+}
+

+ 103 - 1
src/main/java/com/jeesite/modules/mes/web/ErpProductionOrderController.java

@@ -1,9 +1,16 @@
 package com.jeesite.modules.mes.web;
 
 import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
 import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.erp.ErpPickMaterialBill;
+import com.jeesite.modules.mes.erp.ErpPickMaterialBillService;
+import com.jeesite.modules.mes.erp.ErpPrdInStock;
+import com.jeesite.modules.mes.erp.ErpPrdInStockService;
 import com.jeesite.modules.mes.erp.ErpProductionOrder;
 import com.jeesite.modules.mes.erp.ErpProductionOrderService;
+import com.jeesite.modules.mes.erp.ErpTransferDirect;
+import com.jeesite.modules.mes.erp.ErpTransferDirectService;
 import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
@@ -12,16 +19,28 @@ import org.springframework.web.bind.annotation.ResponseBody;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 @Controller
 @RequestMapping(value = "${adminPath}/mes/erpProductionOrder")
 public class ErpProductionOrderController extends BaseController {
 
 	private final ErpProductionOrderService erpProductionOrderService;
+	private final ErpPickMaterialBillService erpPickMaterialBillService;
+	private final ErpPrdInStockService erpPrdInStockService;
+	private final ErpTransferDirectService erpTransferDirectService;
 
-	public ErpProductionOrderController(ErpProductionOrderService erpProductionOrderService) {
+	public ErpProductionOrderController(ErpProductionOrderService erpProductionOrderService,
+			ErpPickMaterialBillService erpPickMaterialBillService,
+			ErpPrdInStockService erpPrdInStockService,
+			ErpTransferDirectService erpTransferDirectService) {
 		this.erpProductionOrderService = erpProductionOrderService;
+		this.erpPickMaterialBillService = erpPickMaterialBillService;
+		this.erpPrdInStockService = erpPrdInStockService;
+		this.erpTransferDirectService = erpTransferDirectService;
 	}
 
 	@RequiresPermissions("mes:erpProductionOrder:view")
@@ -64,4 +83,87 @@ public class ErpProductionOrderController extends BaseController {
 		page.setCount(list.size());
 		return page;
 	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "downstream")
+	public String downstream(ErpProductionOrder erpProductionOrder, HttpServletRequest request, Model model) {
+		String billNo = erpProductionOrder == null ? "" : erpProductionOrder.getBillNo();
+		if (StringUtils.isBlank(billNo) || !erpProductionOrderService.isVisibleProductionOrder(billNo.trim())) {
+			model.addAttribute("message", "生产订单不存在或不在 MES 可见范围内");
+		}
+		billNo = StringUtils.trim(billNo);
+		model.addAttribute("billNo", billNo);
+		model.addAttribute("modal", Boolean.TRUE.equals(
+				"true".equalsIgnoreCase(StringUtils.trim(request.getParameter("modal")))));
+		ErpProductionOrder query = new ErpProductionOrder();
+		query.setBillNo(billNo);
+		model.addAttribute("erpProductionOrder", query);
+		return "modules/mes/erpProductionOrderDownstream";
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "downstreamPickData")
+	@ResponseBody
+	public Page<ErpPickMaterialBill> downstreamPickData(ErpProductionOrder erpProductionOrder,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<ErpPickMaterialBill> page = new Page<>(request, response);
+		String billNo = erpProductionOrder == null ? "" : erpProductionOrder.getBillNo();
+		if (StringUtils.isBlank(billNo) || !erpProductionOrderService.isVisibleProductionOrder(billNo.trim())) {
+			return page;
+		}
+		List<ErpPickMaterialBill> list = erpPickMaterialBillService.findTraceListByMoBillNo(billNo.trim());
+		page.setList(list);
+		page.setCount(list.size());
+		return page;
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "downstreamPrdInStockData")
+	@ResponseBody
+	public Page<ErpPrdInStock> downstreamPrdInStockData(ErpProductionOrder erpProductionOrder,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<ErpPrdInStock> page = new Page<>(request, response);
+		String billNo = erpProductionOrder == null ? "" : erpProductionOrder.getBillNo();
+		if (StringUtils.isBlank(billNo) || !erpProductionOrderService.isVisibleProductionOrder(billNo.trim())) {
+			return page;
+		}
+		List<ErpPrdInStock> list = erpPrdInStockService.findListByMoBillNo(billNo.trim());
+		page.setList(list);
+		page.setCount(list.size());
+		return page;
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "downstreamTransferDirectData")
+	@ResponseBody
+	public Page<ErpTransferDirect> downstreamTransferDirectData(ErpProductionOrder erpProductionOrder,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<ErpTransferDirect> page = new Page<>(request, response);
+		String billNo = erpProductionOrder == null ? "" : erpProductionOrder.getBillNo();
+		if (StringUtils.isBlank(billNo) || !erpProductionOrderService.isVisibleProductionOrder(billNo.trim())) {
+			return page;
+		}
+		billNo = billNo.trim();
+		List<ErpProductionOrder> moDetails = erpProductionOrderService.findDetailList(billNo);
+		String moFid = moDetails.isEmpty() ? "" : moDetails.get(0).getFid();
+		List<ErpPrdInStock> instockList = erpPrdInStockService.findListByMoBillNo(billNo);
+		List<String> instockLots = extractInstockLots(instockList);
+		List<ErpTransferDirect> list = erpTransferDirectService.findTraceListByMoBillNo(billNo, moFid, instockLots);
+		page.setList(list);
+		page.setCount(list.size());
+		return page;
+	}
+
+	private List<String> extractInstockLots(List<ErpPrdInStock> instockList) {
+		Set<String> lots = new LinkedHashSet<>();
+		if (instockList != null) {
+			for (ErpPrdInStock item : instockList) {
+				if (item == null || StringUtils.isBlank(item.getLotNumber())) {
+					continue;
+				}
+				lots.add(item.getLotNumber().trim());
+			}
+		}
+		return new ArrayList<>(lots);
+	}
 }

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

@@ -0,0 +1,188 @@
+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.erp.ErpPrdInStock;

+import com.jeesite.modules.mes.erp.ErpPrdInStockService;

+import com.jeesite.modules.mes.erp.ErpProductionOrder;

+import com.jeesite.modules.mes.erp.ErpProductionOrderService;

+import com.jeesite.modules.mes.erp.ErpStockLocOption;

+import com.jeesite.modules.mes.erp.ErpStockLocService;

+import com.jeesite.modules.mes.erp.ErpStockOption;

+import com.jeesite.modules.mes.erp.ErpStockService;

+import com.jeesite.modules.mes.erp.ErpTransferDirect;

+import com.jeesite.modules.mes.erp.ErpTransferDirectResult;

+import com.jeesite.modules.mes.erp.ErpTransferDirectSaveService;

+import com.jeesite.modules.mes.erp.ErpTransferDirectService;

+import com.jeesite.modules.mes.erp.ErpTransferDirectSubmitRequest;

+import com.jeesite.modules.mes.erp.KingdeeErpProperties;

+import org.apache.shiro.authz.annotation.RequiresPermissions;

+import org.springframework.stereotype.Controller;

+import org.springframework.ui.Model;

+import org.springframework.web.bind.annotation.GetMapping;

+import org.springframework.web.bind.annotation.PostMapping;

+import org.springframework.web.bind.annotation.RequestBody;

+import org.springframework.web.bind.annotation.RequestMapping;

+import org.springframework.web.bind.annotation.ResponseBody;

+

+import javax.servlet.http.HttpServletRequest;

+import javax.servlet.http.HttpServletResponse;

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+

+/**

+ * ERP 直接调拨单查询与新增。

+ */

+@Controller

+@RequestMapping(value = "${adminPath}/mes/erpTransferDirect")

+public class ErpTransferDirectController extends BaseController {

+

+	private final ErpTransferDirectService erpTransferDirectService;

+	private final ErpTransferDirectSaveService erpTransferDirectSaveService;

+	private final ErpProductionOrderService erpProductionOrderService;

+	private final ErpPrdInStockService erpPrdInStockService;

+	private final ErpStockService erpStockService;

+	private final ErpStockLocService erpStockLocService;

+	private final KingdeeErpProperties erpProperties;

+

+	public ErpTransferDirectController(ErpTransferDirectService erpTransferDirectService,

+			ErpTransferDirectSaveService erpTransferDirectSaveService,

+			ErpProductionOrderService erpProductionOrderService,

+			ErpPrdInStockService erpPrdInStockService,

+			ErpStockService erpStockService,

+			ErpStockLocService erpStockLocService,

+			KingdeeErpProperties erpProperties) {

+		this.erpTransferDirectService = erpTransferDirectService;

+		this.erpTransferDirectSaveService = erpTransferDirectSaveService;

+		this.erpProductionOrderService = erpProductionOrderService;

+		this.erpPrdInStockService = erpPrdInStockService;

+		this.erpStockService = erpStockService;

+		this.erpStockLocService = erpStockLocService;

+		this.erpProperties = erpProperties;

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = {"list", ""})

+	public String list(ErpTransferDirect erpTransferDirect, Model model) {

+		model.addAttribute("erpTransferDirect", erpTransferDirect);

+		return "modules/mes/erpTransferDirectList";

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "listData")

+	@ResponseBody

+	public Page<ErpTransferDirect> listData(ErpTransferDirect erpTransferDirect,

+			HttpServletRequest request, HttpServletResponse response) {

+		Page<ErpTransferDirect> page = new Page<>(request, response);

+		int pageSize = page.getPageSize() > 0 ? page.getPageSize() : 20;

+		int pageNo = page.getPageNo() > 0 ? page.getPageNo() : 1;

+		int startRow = (pageNo - 1) * pageSize;

+		List<ErpTransferDirect> list = erpTransferDirectService.findPageList(erpTransferDirect, startRow, pageSize);

+		page.setList(list);

+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));

+		return page;

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "detail")

+	public String detail(ErpTransferDirect erpTransferDirect, Model model) {

+		model.addAttribute("erpTransferDirect", erpTransferDirect);

+		return "modules/mes/erpTransferDirectDetail";

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "detailData")

+	@ResponseBody

+	public Page<ErpTransferDirect> detailData(ErpTransferDirect erpTransferDirect,

+			HttpServletRequest request, HttpServletResponse response) {

+		Page<ErpTransferDirect> page = new Page<>(request, response);

+		List<ErpTransferDirect> list = erpTransferDirectService.findDetailList(erpTransferDirect.getBillNo());

+		page.setList(list);

+		page.setCount(list.size());

+		return page;

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "selectOrder")

+	public String selectOrder(ErpProductionOrder erpProductionOrder, Model model) {

+		model.addAttribute("erpProductionOrder", erpProductionOrder);

+		return "modules/mes/erpTransferDirectSelectOrder";

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "selectOrderData")

+	@ResponseBody

+	public Page<ErpProductionOrder> selectOrderData(ErpProductionOrder erpProductionOrder,

+			HttpServletRequest request, HttpServletResponse response) {

+		Page<ErpProductionOrder> page = new Page<>(request, response);

+		int pageSize = page.getPageSize() > 0 ? page.getPageSize() : 20;

+		int pageNo = page.getPageNo() > 0 ? page.getPageNo() : 1;

+		int startRow = (pageNo - 1) * pageSize;

+		List<ErpProductionOrder> list = erpProductionOrderService.findPageList(

+				erpProductionOrder.getBillNo(), startRow, pageSize);

+		page.setList(list);

+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));

+		return page;

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "form")

+	public String form(String moBillNo, Model model) {

+		String billNo = StringUtils.trim(moBillNo);

+		List<ErpPrdInStock> sourceList = erpPrdInStockService.findTransferSourceByMoBillNo(billNo);

+		List<ErpStockOption> stockList = erpStockService.findUsableStockList();

+		model.addAttribute("moBillNo", billNo);

+		model.addAttribute("sourceList", sourceList);

+		model.addAttribute("stockList", stockList);

+		model.addAttribute("defaultSrcStockNumber", erpStockService.resolveDefaultStockNumber(stockList));

+		model.addAttribute("defaultDestStockNumber", StringUtils.trim(erpProperties.getTransferDirectDefaultDestStockNumber()));

+		if (sourceList.isEmpty()) {

+			model.addAttribute("message", "该生产订单暂无 HEV/115 生产入库行,请先完成生产入库");

+		}

+		return "modules/mes/erpTransferDirectForm";

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@RequestMapping(value = "instockSourceData")

+	@ResponseBody

+	public Page<ErpPrdInStock> instockSourceData(String moBillNo,

+			HttpServletRequest request, HttpServletResponse response) {

+		Page<ErpPrdInStock> page = new Page<>(request, response);

+		List<ErpPrdInStock> list = erpPrdInStockService.findTransferSourceByMoBillNo(moBillNo);

+		page.setList(list);

+		page.setCount(list.size());

+		return page;

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@GetMapping(value = "stockLocOptions")

+	@ResponseBody

+	public Map<String, Object> stockLocOptions(String stockNumber) {

+		List<ErpStockLocOption> locations = erpStockLocService.findLocationsByStock(stockNumber);

+		Map<String, Object> result = new HashMap<>(4);

+		boolean required = !locations.isEmpty();

+		result.put("required", required);

+		result.put("locations", locations);

+		result.put("defaultLocNumber", erpStockLocService.resolveDefaultLocNumber(stockNumber, locations));

+		if (locations.isEmpty()) {

+			result.put("locHint", "该仓库未启用仓位或未配置仓位值集");

+		} else {

+			result.put("locHint", "共 " + locations.size() + " 个仓位");

+		}

+		return result;

+	}

+

+	@RequiresPermissions("mes:erpTransferDirect:view")

+	@PostMapping(value = "submit")

+	@ResponseBody

+	public String submit(@RequestBody ErpTransferDirectSubmitRequest request) {

+		ErpTransferDirectResult result = erpTransferDirectSaveService.submit(request);

+		if (result.isSuccess()) {

+			return renderResult(Global.TRUE, text("直接调拨单保存成功,单号:{0}", result.getBillNo()));

+		}

+		return renderResult(Global.FALSE, result.getMessage());

+	}

+}


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

@@ -80,6 +80,29 @@ erp:
       price-time-point: "1"
       stock-loc-key: F100004
       stock-loc-number: A
+    # 直接调拨单 STK_TransferDirect(固定 HEV / 115 物料,外发调拨)
+    transfer-direct:
+      bill-type: ZJDB01_SYS
+      material-numbers: 8.10.HEV.10.047696,8.35.115.10.5003146600021
+      transfer-biz-type: InnerOrgTransfer
+      transfer-direct-type: GENERAL
+      transfer-category: "3"
+      bill-sign-status: A
+      stock-status: KCZT01_SYS
+      owner-type: BD_OwnerOrg
+      keeper-type: BD_KeeperOrg
+      default-src-stock-number:
+      default-dest-stock-number:
+    # 生产入库单 PRD_INSTOCK 查询(固定 HEV / 115 物料,与直接调拨单一致)
+    prd-in-stock:
+      material-numbers: 8.10.HEV.10.047696,8.35.115.10.5003146600021
+      # Save 配置(单据类型/入库类型需与 ERP 账套一致,Save 报错时可调整)
+      bill-type: SCRKD01_SYS
+      in-stock-type: "1"
+      stock-status: KCZT01_SYS
+      owner-type: BD_OwnerOrg
+      keeper-type: BD_KeeperOrg
+      default-stock-number:
 
 #======================================#
 #========== Server settings ===========#

+ 30 - 0
src/main/resources/mappings/modules/mes/MesBatchSnDao.xml

@@ -20,4 +20,34 @@
 		ORDER BY ID DESC
 		LIMIT 1
 	</select>
+
+	<!-- 待 ERP 生产入库批次:nums>0 且 instock_status 为空或 0 -->
+	<select id="findPendingInstockList" resultType="MesBatchSn">
+		SELECT
+			a.id AS "id",
+			a.batch_sn AS "batchSn",
+			a.nums AS "nums",
+			a.is_js AS "isJs",
+			a.line_sn AS "lineSn",
+			a.instock_status AS "instockStatus",
+			a.mo_bill_no AS "moBillNo",
+			a.mo_entry_id AS "moEntryId",
+			a.erp_instock_bill_no AS "erpInstockBillNo",
+			a.erp_lot_number AS "erpLotNumber",
+			a.erp_instock_date AS "erpInstockDate",
+			a.create_by AS "createBy",
+			a.create_date AS "createDate",
+			a.update_by AS "updateBy",
+			a.update_date AS "updateDate"
+		FROM mes_batch_sn a
+		WHERE a.nums &gt; 0
+		  AND (a.instock_status IS NULL OR a.instock_status = '' OR a.instock_status = '0')
+		<if test="lineSnList != null and lineSnList.size() &gt; 0">
+			AND a.line_sn IN
+			<foreach collection="lineSnList" item="lineSn" open="(" separator="," close=")">
+				#{lineSn}
+			</foreach>
+		</if>
+		ORDER BY a.create_date ASC
+	</select>
 </mapper>

+ 41 - 0
src/main/resources/views/modules/mes/erpPrdInStockDetail.html

@@ -0,0 +1,41 @@
+<% layout('/layouts/default.html', {title: 'ERP生产入库单明细', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa fa-list"></i> ${text('ERP生产入库单明细')}:${erpPrdInStock.billNo}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpPrdInStock/list" class="btn btn-default" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${erpPrdInStock}" action="${ctx}/mes/erpPrdInStock/detailData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<#form:hidden path="billNo"/>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("行号")}', name:'seq', index:'seq', width:70, align:"center", frozen:true},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:200, align:"left", frozen:true},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:120, align:"left"},
+		{header:'${text("生产订单号")}', name:'moBillNo', index:'moBillNo', width:170, align:"center"},
+		{header:'${text("入库数量")}', name:'realQty', index:'realQty', width:100, align:"right", formatter: formatErpQty},
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:80, align:"center"},
+		{header:'${text("仓库")}', name:'stockName', index:'stockName', width:140, align:"left"},
+		{header:'${text("批号")}', name:'lotNumber', index:'lotNumber', width:130, align:"left"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"},
+		{header:'${text("入库日期")}', name:'date', index:'date', width:100, align:"center"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 170 - 0
src/main/resources/views/modules/mes/erpPrdInStockForm.html

@@ -0,0 +1,170 @@
+<% layout('/layouts/default.html', {title: '新增生产入库', libs: []}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa fa-plus"></i> ${text('增加生产入库')}:${moBillNo!}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="javascript:void(0)" class="btn btn-primary" id="btnSubmitInstock"><i class="fa fa-check"></i> ${text('提交入库')}</a>
+				<a href="${ctx}/mes/erpPrdInStock/selectDetail?billNo=${moBillNo!}" class="btn btn-default addTabPage" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+			</div>
+		</div>
+		<div class="box-body">
+			<% if (isNotBlank(message!)) { %>
+			<div class="alert alert-warning">${message}</div>
+			<% } %>
+			<div class="alert alert-info">
+				<i class="fa fa-info-circle"></i>
+				${text('勾选 MES 托盘批次码,入库数量=已绑定数量(nums)合计;ERP 批号由 ERP 系统内部生成。产线:HEVXT→HEV,115XT→115;须 nums>0。')}
+			</div>
+			<% if(moEntry != null){ %>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6"><label class="control-label">${text('物料')}:</label> ${moEntry.materialNumber!} ${moEntry.materialName!}</div>
+				<div class="col-xs-6 col-sm-2"><label class="control-label">${text('订单行号')}:</label> ${moEntry.seq!}</div>
+				<div class="col-xs-6 col-sm-2"><label class="control-label">${text('订单数量')}:</label> ${moEntry.qty!}</div>
+				<div class="col-xs-6 col-sm-2"><label class="control-label">${text('已入库')}:</label> ${moEntry.instockQty!0}</div>
+				<div class="col-xs-6 col-sm-2"><label class="control-label">${text('未入库')}:</label> <strong class="text-danger">${moEntry.remainQty!0}</strong></div>
+				<div class="col-xs-6 col-sm-2"><label class="control-label">${text('本次合计')}:</label> <strong id="totalQtyLabel">0</strong></div>
+			</div>
+			<% } %>
+			<div class="table-responsive" style="margin-bottom:16px;">
+				<table class="table table-bordered table-hover table-condensed" id="batchTable">
+					<thead>
+						<tr>
+							<th style="width:40px;"><input type="checkbox" id="checkAllBatch"/></th>
+							<th>${text('批次码')}</th>
+							<th>${text('产线')}</th>
+							<th>${text('已绑定数量')}</th>
+							<th>${text('是否用完')}</th>
+							<th>${text('创建时间')}</th>
+						</tr>
+					</thead>
+					<tbody>
+						<% if(batchList.~size == 0){ %>
+						<tr><td colspan="6" class="text-center text-muted">${text('无待入库批次码')}</td></tr>
+						<% }else{ %>
+						<% for(row in batchList){ %>
+						<tr class="batch-row" data-id="${row.id!}" data-qty="${row.nums!0}">
+							<td class="text-center"><input type="checkbox" name="batchRow" class="batch-check" value="${row.id!}"/></td>
+							<td><strong>${row.batchSn!}</strong></td>
+							<td class="text-center">${row.lineSn!}</td>
+							<td class="text-right">${row.nums!}</td>
+							<td class="text-center">${row.isJs!}</td>
+							<td>${row.createDate!, 'yyyy-MM-dd HH:mm:ss'}</td>
+						</tr>
+						<% } %>
+						<% } %>
+					</tbody>
+				</table>
+			</div>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label"><span class="required">*</span> ${text('入库仓库')}:</label>
+					<% if(stockList.~size == 0){ %>
+					<div class="alert alert-warning" style="margin-top:6px;padding:8px 12px;">${text('未能从 ERP 加载仓库列表')}</div>
+					<% }else{ %>
+					<select id="stockNumber" class="form-control" style="max-width:420px;">
+						<% for(stock in stockList){ %>
+						<option value="${stock.stockNumber!}" <% if(stock.stockNumber == defaultStockNumber){ %>selected<% } %>>${stock.displayLabel!}</option>
+						<% } %>
+					</select>
+					<% } %>
+				</div>
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label">${text('入库仓位')}:</label>
+					<select id="stockLocNumber" class="form-control" style="max-width:420px;">
+						<option value="">${text('请先选择仓库')}</option>
+					</select>
+					<p class="help-block" id="locHint"></p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+var moBillNo = '${moBillNo!}';
+var entryId = '${entryId!}';
+var stockLocUrl = '${ctx}/mes/erpPrdInStock/stockLocOptions';
+var submitUrl = '${ctx}/mes/erpPrdInStock/submit';
+
+function refreshTotalQty() {
+	var total = 0;
+	$('.batch-check:checked').each(function() {
+		var qty = parseFloat($(this).closest('tr').data('qty')) || 0;
+		total += qty;
+	});
+	$('#totalQtyLabel').text(total);
+}
+
+function loadStockLoc(stockNumber) {
+	$('#stockLocNumber').empty().append('<option value="">${text("无仓位")}</option>');
+	$('#locHint').text('');
+	if (!stockNumber) return;
+	$.get(stockLocUrl, { stockNumber: stockNumber }, function(res) {
+		if (!res || !res.locations || res.locations.length === 0) {
+			$('#locHint').text(res && res.locHint ? res.locHint : '');
+			return;
+		}
+		var defaultLoc = res.defaultLocNumber || '';
+		$.each(res.locations, function(i, loc) {
+			var label = loc.locNumber + (loc.locName ? ' ' + loc.locName : '');
+			var selected = (loc.locNumber === defaultLoc) ? ' selected' : '';
+			$('#stockLocNumber').append('<option value="' + loc.locNumber + '"' + selected + '>' + label + '</option>');
+		});
+		$('#locHint').text(res.locHint || '');
+	}, 'json');
+}
+
+$('#checkAllBatch').on('change', function() {
+	$('.batch-check').prop('checked', $(this).is(':checked'));
+	refreshTotalQty();
+});
+$(document).on('change', '.batch-check', refreshTotalQty);
+$('#stockNumber').on('change', function() { loadStockLoc($(this).val()); });
+
+$('#btnSubmitInstock').on('click', function() {
+	var batchIds = [];
+	$('.batch-check:checked').each(function() { batchIds.push($(this).val()); });
+	if (batchIds.length === 0) {
+		js.showMessage('${text("请至少选择一个 MES 批次码")}');
+		return;
+	}
+	var stockNumber = $.trim($('#stockNumber').val());
+	if (!stockNumber) {
+		js.showMessage('${text("请选择入库仓库")}');
+		return;
+	}
+	var payload = {
+		moBillNo: moBillNo,
+		entryId: entryId,
+		batchIds: batchIds,
+		stockNumber: stockNumber,
+		stockLocNumber: $.trim($('#stockLocNumber').val())
+	};
+	$('#btnSubmitInstock').prop('disabled', true);
+	$.ajax({
+		url: submitUrl,
+		type: 'POST',
+		contentType: 'application/json; charset=utf-8',
+		data: JSON.stringify(payload),
+		success: function(res) {
+			$('#btnSubmitInstock').prop('disabled', false);
+			if (res && res.result === 'true') {
+				js.showMessage(res.message, null, 'success');
+			} else {
+				js.showMessage(res && res.message ? res.message : '${text("提交失败")}');
+			}
+		},
+		error: function() {
+			$('#btnSubmitInstock').prop('disabled', false);
+			js.showMessage('${text("请求失败,请检查网络或 ERP 连接")}');
+		}
+	});
+});
+
+$(function() {
+	loadStockLoc($('#stockNumber').val());
+});
+</script>

+ 88 - 0
src/main/resources/views/modules/mes/erpPrdInStockList.html

@@ -0,0 +1,88 @@
+<% layout('/layouts/default.html', {title: 'ERP生产入库单查询', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa fa-sign-in"></i> ${text('ERP生产入库单查询')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpPrdInStock/selectOrder" class="btn btn-primary btnTool" title="${text('增加生产入库')}"><i class="fa fa-plus"></i> ${text('增加')}</a>
+				<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('仅展示物料')} <strong>8.10.HEV.10.047696(HEV)</strong> ${text('与')}
+				<strong>8.35.115.10.5003146600021(115)</strong> ${text('的生产入库明细分录,并按当前组织过滤。')}
+			</div>
+			<#form:form id="searchForm" model="${erpPrdInStock}" action="${ctx}/mes/erpPrdInStock/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="billNoQuery" maxlength="100" class="form-control width-180"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('生产订单号')}:</label>
+					<div class="control-inline">
+						<#form:input path="moBillNoQuery" maxlength="100" class="form-control width-180"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('物料编码')}:</label>
+					<div class="control-inline">
+						<select name="materialNumberQuery" id="materialNumberQuery" class="form-control width-260">
+							<option value="">${text('全部(HEV+115)')}</option>
+							<option value="8.10.HEV.10.047696">8.10.HEV.10.047696(HEV)</option>
+							<option value="8.35.115.10.5003146600021">8.35.115.10.5003146600021(115)</option>
+						</select>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('单据状态')}:</label>
+					<div class="control-inline">
+						<select name="documentStatusQuery" id="documentStatusQuery" class="form-control">
+							<option value="">${text('全部')}</option>
+							<option value="A">${text('创建')}</option>
+							<option value="B">${text('审核中')}</option>
+							<option value="C">${text('已审核')}</option>
+							<option value="D">${text('重新审核')}</option>
+						</select>
+					</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:'billNo', index:'billNo', width:170, align:"center", frozen:true, formatter: function(val){
+			return '<a href="${ctx}/mes/erpPrdInStock/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP生产入库单明细")}">'+val+'</a>';
+		}},
+		{header:'${text("入库日期")}', name:'date', index:'date', width:100, align:"center"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"},
+		{header:'${text("生产订单号")}', name:'moBillNo', index:'moBillNo', width:170, align:"center"},
+		{header:'${text("生产组织")}', name:'prdOrgName', index:'prdOrgName', width:160, align:"left"},
+		{header:'${text("行号")}', name:'seq', index:'seq', width:60, align:"center"},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:200, align:"left"},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:120, align:"left"},
+		{header:'${text("入库数量")}', name:'realQty', index:'realQty', width:90, align:"right", formatter: formatErpQty},
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:60, align:"center"},
+		{header:'${text("仓库")}', name:'stockName', index:'stockName', width:140, align:"left"},
+		{header:'${text("批号")}', name:'lotNumber', index:'lotNumber', width:120, align:"left"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 66 - 0
src/main/resources/views/modules/mes/erpPrdInStockSelectDetail.html

@@ -0,0 +1,66 @@
+<% 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"></i> ${text('增加生产入库 - 选择订单明细')}:${erpProductionOrder.billNo}

+			</div>

+			<div class="box-tools pull-right">

+				<a href="${ctx}/mes/erpPrdInStock/selectOrder" class="btn btn-default addTabPage" title="${text('返回')}"><i class="fa fa-mail-reply"></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('仅展示配置物料分录')}(8.10.HEV.10.047696 / 8.35.115.10.5003146600021)。

+				${text('已入库=ERP 已审核生产入库实收合计;未入库=订单数量-已入库。')}

+			</div>

+			<#form:form id="searchForm" model="${erpProductionOrder}" action="${ctx}/mes/erpPrdInStock/selectDetailData" method="post" class="hide"

+					data-page-no="1" data-page-size="500">

+				<#form:hidden path="billNo"/>

+			</#form:form>

+			<table id="dataGrid"></table>

+			<div id="dataGridPage"></div>

+		</div>

+	</div>

+</div>

+<% } %>

+<script>

+$('#dataGrid').dataGrid({

+	searchForm: $("#searchForm"),

+	columnModel: [

+		{header:'${text("操作")}', name:'actions', width:120, align:"center", frozen:true, formatter: function(val, obj, row){

+			var disabled = row.remainQty != null && parseFloat(row.remainQty) <= 0;

+			if (disabled) {

+				return '<span class="text-muted" title="${text("该分录已无未入库数量")}">${text("已满入")}</span>';

+			}

+			return '<a href="${ctx}/mes/erpPrdInStock/form?moBillNo='+encodeURIComponent(row.billNo)+'&entryId='+encodeURIComponent(row.entryId)+'" class="btnList addTabPage" data-title="${text("新增生产入库")}">'+

+				'<i class="fa fa-check"></i> ${text("选批次入库")}</a>';

+		}},

+		{header:'${text("行号")}', name:'seq', index:'seq', width:70, align:"center", frozen:true},

+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:200, align:"left"},

+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:160, align:"left"},

+		{header:'${text("规格型号")}', name:'specification', index:'specification', width:120, align:"left"},

+		{header:'${text("存货类别")}', name:'inventoryCategory', index:'inventoryCategory', width:100, align:"center"},

+		{header:'${text("订单数量")}', name:'qty', index:'qty', width:100, align:"right", formatter: formatErpQty},

+		{header:'${text("已入库")}', name:'instockQty', index:'instockQty', width:100, align:"right", formatter: formatErpQty},

+		{header:'${text("未入库")}', name:'remainQty', index:'remainQty', width:100, align:"right", formatter: function(val){

+			if (val == null || val === '') return '';

+			var num = parseFloat(val);

+			var text = formatErpQty(val);

+			return num > 0 ? '<strong class="text-danger">'+text+'</strong>' : text;

+		}},

+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:70, align:"center"},

+		{header:'${text("生产车间")}', name:'workshopName', index:'workshopName', width:120, align:"left"},

+		{header:'${text("入库仓库")}', name:'stockName', index:'stockName', width:120, align:"left"},

+		{header:'${text("业务状态")}', name:'businessStatusName', index:'businessStatusName', width:90, align:"center"},

+		{header:'${text("计划开工")}', name:'planStartDate', index:'planStartDate', width:110, align:"center"},

+		{header:'${text("计划完工")}', name:'planFinishDate', index:'planFinishDate', width:110, align:"center"},

+		{header:'${text("BOM")}', name:'bomNumber', index:'bomNumber', width:120, align:"left"},

+		{header:'${text("工艺路线")}', name:'routingNumber', index:'routingNumber', width:120, align:"left"},

+		{header:'${text("项目号")}', name:'projectNo', index:'projectNo', width:100, align:"left"}

+	],

+	ajaxSuccess: function(){}

+});

+</script>


+ 56 - 0
src/main/resources/views/modules/mes/erpPrdInStockSelectOrder.html

@@ -0,0 +1,56 @@
+<% 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="${ctx}/mes/erpPrdInStock/list" class="btn btn-default" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+			</div>
+		</div>
+		<div class="box-body">
+			<div class="alert alert-info" style="margin-bottom:10px;">
+				<i class="fa fa-info-circle"></i>
+				${text('仅展示配置物料')} <strong>8.10.HEV.10.047696(HEV)</strong> ${text('与')}
+				<strong>8.35.115.10.5003146600021(115)</strong> ${text('的生产订单;再选分录、勾选 MES 批次码后推 ERP 生产入库。')}
+			</div>
+			<#form:form id="searchForm" model="${erpProductionOrder}" action="${ctx}/mes/erpPrdInStock/selectOrderData" 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="billNo" maxlength="100" class="form-control width-180"/>
+					</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:'actions', width:100, align:"center", frozen:true, formatter: function(val, obj, row){
+			return '<a href="${ctx}/mes/erpPrdInStock/selectDetail?billNo='+encodeURIComponent(row.billNo)+'" class="btnList addTabPage" data-title="${text("选择订单明细")}">'+
+				'<i class="fa fa-check"></i> ${text("选择")}</a>';
+		}},
+		{header:'${text("生产订单号")}', name:'billNo', index:'billNo', width:180, align:"center", frozen:true},
+		{header:'${text("单据日期")}', name:'date', index:'date', width:120, align:"center"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:100, align:"center"},
+		{header:'${text("生产组织")}', name:'prdOrgName', index:'prdOrgName', width:180, align:"left"},
+		{header:'${text("存货类别")}', name:'inventoryCategory', index:'inventoryCategory', width:120, align:"center"},
+		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:100, align:"right"},
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right", formatter: formatErpQty}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 452 - 0
src/main/resources/views/modules/mes/erpProductionOrderDownstream.html

@@ -0,0 +1,452 @@
+<% layout('/layouts/default.html', {title: '生产订单下查', libs: ['dataGrid']}){ %>
+
+<div class="main-content erp-mo-downstream-page">
+
+	<div class="box box-main">
+
+		<div class="box-header">
+
+			<div class="box-title">
+
+				<i class="fa fa-sitemap"></i> ${text('生产订单下查')}:${billNo!}
+
+			</div>
+
+			<% if (!modal) { %>
+
+			<div class="box-tools pull-right">
+
+				<a href="${ctx}/mes/erpProductionOrder/list" class="btn btn-default" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+
+			</div>
+
+			<% } %>
+
+		</div>
+
+		<div class="box-body">
+
+			<% if (isNotBlank(message!)) { %>
+
+			<div class="alert alert-warning">${message}</div>
+
+			<% } else { %>
+
+			<div class="alert alert-info" style="margin-bottom:10px;padding:8px 12px;">
+
+				<i class="fa fa-info-circle"></i>
+
+				${text('直接调拨单无生产订单号字段,按生产入库批号及源单号关联展示。生产入库与直接调拨单均仅展示 HEV/115 物料。')}
+
+			</div>
+
+			<ul class="nav nav-tabs erp-mo-downstream-tabs" role="tablist">
+
+				<li role="presentation" class="active">
+
+					<a href="#tabPick" aria-controls="tabPick" role="tab" data-toggle="tab">${text('生产领料单')}</a>
+
+				</li>
+
+				<li role="presentation">
+
+					<a href="#tabPrdInStock" aria-controls="tabPrdInStock" role="tab" data-toggle="tab">${text('生产入库单')}</a>
+
+				</li>
+
+				<li role="presentation">
+
+					<a href="#tabTransferDirect" aria-controls="tabTransferDirect" role="tab" data-toggle="tab">${text('直接调拨单')}</a>
+
+				</li>
+
+			</ul>
+
+			<div class="tab-content erp-mo-downstream-tab-content">
+
+				<div role="tabpanel" class="tab-pane active" id="tabPick">
+
+					<#form:form id="pickSearchForm" model="${erpProductionOrder!}" action="${ctx}/mes/erpProductionOrder/downstreamPickData" method="post" class="hide"
+
+							data-page-no="1" data-page-size="500">
+
+						<#form:hidden path="billNo"/>
+
+					</#form:form>
+
+					<table id="pickGrid"></table>
+
+					<div id="pickGridPage"></div>
+
+				</div>
+
+				<div role="tabpanel" class="tab-pane" id="tabPrdInStock">
+
+					<#form:form id="prdInStockSearchForm" model="${erpProductionOrder!}" action="${ctx}/mes/erpProductionOrder/downstreamPrdInStockData" method="post" class="hide"
+
+							data-page-no="1" data-page-size="500">
+
+						<#form:hidden path="billNo"/>
+
+					</#form:form>
+
+					<table id="prdInStockGrid"></table>
+
+					<div id="prdInStockGridPage"></div>
+
+					<div class="erp-mo-downstream-summary">
+
+						<strong>${text('入库数量合计')}:</strong><span id="prdInStockTotalQty">0</span>
+
+					</div>
+
+				</div>
+
+				<div role="tabpanel" class="tab-pane" id="tabTransferDirect">
+
+					<#form:form id="transferDirectSearchForm" model="${erpProductionOrder!}" action="${ctx}/mes/erpProductionOrder/downstreamTransferDirectData" method="post" class="hide"
+
+							data-page-no="1" data-page-size="500">
+
+						<#form:hidden path="billNo"/>
+
+					</#form:form>
+
+					<table id="transferDirectGrid"></table>
+
+					<div id="transferDirectGridPage"></div>
+
+					<div class="erp-mo-downstream-summary">
+
+						<strong>${text('调拨数量合计')}:</strong><span id="transferDirectTotalQty">0</span>
+
+					</div>
+
+				</div>
+
+			</div>
+
+			<% } %>
+
+		</div>
+
+	</div>
+
+</div>
+
+<% } %>
+
+<style>
+
+.erp-mo-downstream-tabs { margin-bottom: 12px; }
+
+.erp-mo-downstream-tab-content { min-height: 360px; }
+
+.erp-mo-downstream-summary {
+
+	padding: 10px 12px;
+
+	margin-top: 8px;
+
+	border-top: 1px solid #e5e5e5;
+
+	background: #fafafa;
+
+	text-align: right;
+
+	font-size: 14px;
+
+}
+
+</style>
+
+<script>
+
+<% if (isBlank(message!)) { %>
+
+var prdInStockGridReady = false;
+
+var transferDirectGridReady = false;
+
+
+
+function downstreamGridHeight() {
+
+	return Math.max(320, $(window).height() - ($('.erp-mo-downstream-page .box-header').outerHeight(true) || 0)
+
+		- ($('.erp-mo-downstream-tabs').outerHeight(true) || 0)
+
+		- ($('.erp-mo-downstream-page .alert-info').outerHeight(true) || 0)
+
+		- ($('#pickGridPage:visible, #prdInStockGridPage:visible, #transferDirectGridPage:visible').outerHeight(true) || 0) - 150);
+
+}
+
+
+
+function refreshPrdInStockTotal(data) {
+
+	var total = 0;
+
+	if (data && data.list) {
+
+		for (var i = 0; i < data.list.length; i++) {
+
+			var qty = parseFloat(data.list[i].realQty);
+
+			if (!isNaN(qty)) total += qty;
+
+		}
+
+	}
+
+	$('#prdInStockTotalQty').text(typeof formatErpQty === 'function' ? formatErpQty(total) : total);
+
+}
+
+
+
+function refreshTransferDirectTotal(data) {
+
+	var total = 0;
+
+	if (data && data.list) {
+
+		for (var i = 0; i < data.list.length; i++) {
+
+			var qty = parseFloat(data.list[i].qty);
+
+			if (!isNaN(qty)) total += qty;
+
+		}
+
+	}
+
+	$('#transferDirectTotalQty').text(typeof formatErpQty === 'function' ? formatErpQty(total) : total);
+
+}
+
+
+
+function initPrdInStockGrid() {
+
+	if (prdInStockGridReady) {
+
+		$('#prdInStockGrid').dataGrid('refresh');
+
+		$('#prdInStockGrid').jqGrid('setGridWidth', $('#tabPrdInStock').width(), true);
+
+		return;
+
+	}
+
+	prdInStockGridReady = true;
+
+	$('#prdInStockGrid').dataGrid({
+
+		searchForm: $('#prdInStockSearchForm'),
+
+		dataGridPage: $('#prdInStockGridPage'),
+
+		autoGridHeight: downstreamGridHeight,
+
+		columnModel: [
+
+			{header:'${text("入库单号")}', name:'billNo', width:170, align:"center"},
+
+			{header:'${text("入库日期")}', name:'date', width:100, align:"center"},
+
+			{header:'${text("单据状态")}', name:'documentStatusName', width:90, align:"center"},
+
+			{header:'${text("行号")}', name:'seq', width:60, align:"center"},
+
+			{header:'${text("物料编码")}', name:'materialNumber', width:180, align:"left"},
+
+			{header:'${text("物料名称")}', name:'materialName', width:160, align:"left"},
+
+			{header:'${text("入库数量")}', name:'realQty', width:90, align:"right", formatter: formatErpQty},
+
+			{header:'${text("单位")}', name:'unitName', width:60, align:"center"},
+
+			{header:'${text("入库仓库")}', name:'stockName', width:120, align:"left"},
+
+			{header:'${text("批号")}', name:'lotNumber', width:120, align:"left"}
+
+		],
+
+		ajaxSuccess: function(data) {
+
+			refreshPrdInStockTotal(data);
+
+		},
+
+		ajaxError: function() {
+
+			refreshPrdInStockTotal(null);
+
+		}
+
+	});
+
+	$('#prdInStockGrid').jqGrid('setGridWidth', $('#tabPrdInStock').width(), true);
+
+}
+
+
+
+function initTransferDirectGrid() {
+
+	if (transferDirectGridReady) {
+
+		$('#transferDirectGrid').dataGrid('refresh');
+
+		$('#transferDirectGrid').jqGrid('setGridWidth', $('#tabTransferDirect').width(), true);
+
+		return;
+
+	}
+
+	transferDirectGridReady = true;
+
+	$('#transferDirectGrid').dataGrid({
+
+		searchForm: $('#transferDirectSearchForm'),
+
+		dataGridPage: $('#transferDirectGridPage'),
+
+		autoGridHeight: downstreamGridHeight,
+
+		columnModel: [
+
+			{header:'${text("调拨单号")}', name:'billNo', width:170, align:"center"},
+
+			{header:'${text("调拨日期")}', name:'date', width:100, align:"center"},
+
+			{header:'${text("单据状态")}', name:'documentStatusName', width:90, align:"center"},
+
+			{header:'${text("行号")}', name:'seq', width:60, align:"center"},
+
+			{header:'${text("物料编码")}', name:'materialNumber', width:180, align:"left"},
+
+			{header:'${text("物料名称")}', name:'materialName', width:160, align:"left"},
+
+			{header:'${text("调拨数量")}', name:'qty', width:90, align:"right", formatter: formatErpQty},
+
+			{header:'${text("单位")}', name:'unitName', width:60, align:"center"},
+
+			{header:'${text("调出仓库")}', name:'srcStockName', width:120, align:"left"},
+
+			{header:'${text("调入仓库")}', name:'destStockName', width:120, align:"left"},
+
+			{header:'${text("批号")}', name:'lotNumber', width:120, align:"left"},
+
+			{header:'${text("源单编号")}', name:'srcBillNo', width:140, align:"center"}
+
+		],
+
+		ajaxSuccess: function(data) {
+
+			refreshTransferDirectTotal(data);
+
+		},
+
+		ajaxError: function() {
+
+			refreshTransferDirectTotal(null);
+
+		}
+
+	});
+
+	$('#transferDirectGrid').jqGrid('setGridWidth', $('#tabTransferDirect').width(), true);
+
+}
+
+
+
+$('#pickGrid').dataGrid({
+
+	searchForm: $('#pickSearchForm'),
+
+	dataGridPage: $('#pickGridPage'),
+
+	autoGridHeight: downstreamGridHeight,
+
+	columnModel: [
+
+		{header:'${text("领料单号")}', name:'billNo', width:170, align:"center"},
+
+		{header:'${text("领料日期")}', name:'date', width:100, align:"center"},
+
+		{header:'${text("单据状态")}', name:'documentStatusName', width:90, align:"center"},
+
+		{header:'${text("行号")}', name:'seq', width:60, align:"center"},
+
+		{header:'${text("物料编码")}', name:'materialNumber', width:180, align:"left"},
+
+		{header:'${text("物料名称")}', name:'materialName', width:160, align:"left"},
+
+		{header:'${text("申请数量")}', name:'appQty', width:90, align:"right", formatter: formatErpQty},
+
+		{header:'${text("实发数量")}', name:'actualQty', width:90, align:"right", formatter: formatErpQty},
+
+		{header:'${text("单位")}', name:'unitName', width:60, align:"center"},
+
+		{header:'${text("发料仓库")}', name:'stockName', width:120, align:"left"},
+
+		{header:'${text("用料清单")}', name:'ppbomBillNo', width:140, align:"center"}
+
+	]
+
+});
+
+
+
+$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
+
+	var target = $(e.target).attr('href');
+
+	if (target === '#tabPick') {
+
+		$('#pickGrid').jqGrid('setGridWidth', $('#tabPick').width(), true);
+
+	} else if (target === '#tabPrdInStock') {
+
+		initPrdInStockGrid();
+
+	} else if (target === '#tabTransferDirect') {
+
+		initTransferDirectGrid();
+
+	}
+
+});
+
+
+
+$(window).resize(function() {
+
+	if ($('#pickGrid')[0] && $('#pickGrid')[0].grid) {
+
+		$('#pickGrid').jqGrid('setGridHeight', downstreamGridHeight());
+
+	}
+
+	if ($('#prdInStockGrid')[0] && $('#prdInStockGrid')[0].grid) {
+
+		$('#prdInStockGrid').jqGrid('setGridHeight', downstreamGridHeight());
+
+	}
+
+	if ($('#transferDirectGrid')[0] && $('#transferDirectGrid')[0].grid) {
+
+		$('#transferDirectGrid').jqGrid('setGridHeight', downstreamGridHeight());
+
+	}
+
+});
+
+<% } %>
+
+</script>
+

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

@@ -31,6 +31,19 @@
 </div>
 <% } %>
 <script>
+function openMoDownstreamModal(billNo) {
+	if (!billNo) return;
+	js.layer.open({
+		type: 2,
+		title: '${text("生产订单下查")} - ' + billNo,
+		area: ['92%', '88%'],
+		maxmin: true,
+		shadeClose: true,
+		scrollbar: true,
+		content: '${ctx}/mes/erpProductionOrder/downstream?billNo=' + encodeURIComponent(billNo) + '&modal=true'
+	});
+}
+
 $('#dataGrid').dataGrid({
 	searchForm: $("#searchForm"),
 	columnModel: [
@@ -43,8 +56,18 @@ $('#dataGrid').dataGrid({
 		{header:'${text("存货类别")}', name:'inventoryCategory', index:'inventoryCategory', width:120, align:"center"},
 		{header:'${text("生产车间")}', name:'workshopName', index:'workshopName', width:140, align:"left"},
 		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:100, align:"right"},
-		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right", formatter: formatErpQty}
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right", formatter: formatErpQty},
+		{header:'${text("操作")}', name:'actions', width:80, align:"center", sortable:false, frozen:false, formatter: function(val, obj, row){
+			return '<a href="javascript:" class="btn btn-primary btn-xs btn-mo-downstream" data-bill-no="'+row.billNo+'" title="${text("下查")}">'+
+				'<i class="fa fa-sitemap"></i> ${text("下查")}</a>';
+		}}
 	],
 	ajaxSuccess: function(){}
 });
+
+$(document).on('click', '.btn-mo-downstream', function(e) {
+	e.preventDefault();
+	e.stopPropagation();
+	openMoDownstreamModal($(this).attr('data-bill-no'));
+});
 </script>

+ 42 - 0
src/main/resources/views/modules/mes/erpTransferDirectDetail.html

@@ -0,0 +1,42 @@
+<% layout('/layouts/default.html', {title: 'ERP直接调拨单明细', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa fa-list"></i> ${text('ERP直接调拨单明细')}:${erpTransferDirect.billNo}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpTransferDirect/list" class="btn btn-default" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${erpTransferDirect}" action="${ctx}/mes/erpTransferDirect/detailData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<#form:hidden path="billNo"/>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("行号")}', name:'seq', index:'seq', width:70, align:"center", frozen:true},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:200, align:"left", frozen:true},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:120, align:"left"},
+		{header:'${text("调拨数量")}', name:'qty', index:'qty', width:100, align:"right", formatter: formatErpQty},
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:80, align:"center"},
+		{header:'${text("调出仓库")}', name:'srcStockName', index:'srcStockName', width:140, align:"left"},
+		{header:'${text("调入仓库")}', name:'destStockName', index:'destStockName', width:160, align:"left"},
+		{header:'${text("批号")}', name:'lotNumber', index:'lotNumber', width:130, align:"left"},
+		{header:'${text("备注")}', name:'note', index:'note', width:180, align:"left"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"},
+		{header:'${text("调拨日期")}', name:'date', index:'date', width:100, align:"center"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 255 - 0
src/main/resources/views/modules/mes/erpTransferDirectForm.html

@@ -0,0 +1,255 @@
+<% layout('/layouts/default.html', {title: '新增直接调拨单', libs: []}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa fa-plus"></i> ${text('增加直接调拨单')}:${moBillNo!}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="javascript:void(0)" class="btn btn-primary" id="btnSubmitTransfer"><i class="fa fa-check"></i> ${text('提交调拨')}</a>
+				<a href="${ctx}/mes/erpTransferDirect/selectOrder" class="btn btn-default addTabPage" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+			</div>
+		</div>
+		<div class="box-body">
+			<% if (isNotBlank(message!)) { %>
+			<div class="alert alert-warning">${message}</div>
+			<% } %>
+			<div class="alert alert-info">
+				<i class="fa fa-info-circle"></i>
+				${text('外发调拨:批号锁定为生产入库批号(与生产订单下查关联)。请选择一行入库明细,填写调拨数量及调出/调入仓库仓位后提交。')}
+			</div>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label">${text('生产订单')}:</label>
+					<span>${moBillNo!}</span>
+				</div>
+			</div>
+			<div class="table-responsive" style="margin-bottom:16px;">
+				<table class="table table-bordered table-hover table-condensed" id="sourceTable">
+					<thead>
+						<tr>
+							<th style="width:40px;"></th>
+							<th>${text('入库单号')}</th>
+							<th>${text('行号')}</th>
+							<th>${text('物料编码')}</th>
+							<th>${text('物料名称')}</th>
+							<th>${text('批号')}</th>
+							<th>${text('入库数量')}</th>
+							<th>${text('单位')}</th>
+							<th>${text('仓库')}</th>
+						</tr>
+					</thead>
+					<tbody>
+						<% if(sourceList.~size == 0){ %>
+						<tr><td colspan="9" class="text-center text-muted">${text('无可用生产入库行')}</td></tr>
+						<% }else{ %>
+						<% for(row in sourceList){ %>
+						<tr class="source-row" data-entry-id="${row.entryId!}" data-bill-no="${row.billNo!}"
+							data-material="${row.materialNumber!}" data-lot="${row.lotNumber!}"
+							data-qty="${row.realQty!}" data-stock="${row.stockNumber!}">
+							<td class="text-center">
+								<input type="radio" name="instockRow" class="instock-radio" value="${row.entryId!}"/>
+							</td>
+							<td>${row.billNo!}</td>
+							<td class="text-center">${row.seq!}</td>
+							<td>${row.materialNumber!}</td>
+							<td>${row.materialName!}</td>
+							<td><strong>${row.lotNumber!}</strong></td>
+							<td class="text-right">${row.realQty!}</td>
+							<td class="text-center">${row.unitName!}</td>
+							<td>${row.stockName!} (${row.stockNumber!})</td>
+						</tr>
+						<% } %>
+						<% } %>
+					</tbody>
+				</table>
+			</div>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-4">
+					<label class="control-label"><span class="required">*</span> ${text('调拨数量')}:</label>
+					<input type="number" id="transferQty" class="form-control" min="0" step="any" placeholder="${text('不超过所选入库数量')}"/>
+				</div>
+			</div>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label"><span class="required">*</span> ${text('调出仓库')}:</label>
+					<% if(stockList.~size == 0){ %>
+					<div class="alert alert-warning" style="margin-top:6px;padding:8px 12px;">${text('未能从 ERP 加载仓库列表')}</div>
+					<% }else{ %>
+					<select id="srcStockNumber" class="form-control" style="max-width:420px;">
+						<% for(stock in stockList){ %>
+						<option value="${stock.stockNumber!}">${stock.displayLabel!}</option>
+						<% } %>
+					</select>
+					<% } %>
+				</div>
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label">${text('调出仓位')}:</label>
+					<select id="srcStockLocNumber" class="form-control" style="max-width:420px;">
+						<option value="">${text('请先选择调出仓库')}</option>
+					</select>
+					<p class="help-block" id="srcLocHint"></p>
+				</div>
+			</div>
+			<div class="row" style="margin-bottom:12px;">
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label"><span class="required">*</span> ${text('调入仓库')}:</label>
+					<% if(stockList.~size == 0){ %>
+					<% }else{ %>
+					<select id="destStockNumber" class="form-control" style="max-width:420px;">
+						<option value="">${text('请选择')}</option>
+						<% for(stock in stockList){ %>
+						<option value="${stock.stockNumber!}" <% if(stock.stockNumber == defaultDestStockNumber){ %>selected<% } %>>${stock.displayLabel!}</option>
+						<% } %>
+					</select>
+					<% } %>
+				</div>
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label">${text('调入仓位')}:</label>
+					<select id="destStockLocNumber" class="form-control" style="max-width:420px;">
+						<option value="">${text('请先选择调入仓库')}</option>
+					</select>
+					<p class="help-block" id="destLocHint"></p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+var moBillNo = '${moBillNo!}';
+var stockLocUrl = '${ctx}/mes/erpTransferDirect/stockLocOptions';
+var submitUrl = '${ctx}/mes/erpTransferDirect/submit';
+var defaultSrcStock = '${defaultSrcStockNumber!}';
+
+function getSelectedRow() {
+	var entryId = $('input[name="instockRow"]:checked').val();
+	if (!entryId) return null;
+	var $tr = $('input[name="instockRow"]:checked').closest('tr');
+	return {
+		entryId: entryId,
+		billNo: $tr.data('bill-no'),
+		lotNumber: $tr.data('lot'),
+		maxQty: parseFloat($tr.data('qty')) || 0,
+		stockNumber: $tr.data('stock') || ''
+	};
+}
+
+function loadStockLoc(stockNumber, $select, $hint, placeholder) {
+	$select.empty().append('<option value="">' + placeholder + '</option>');
+	$hint.text('');
+	if (!stockNumber) return;
+	$.get(stockLocUrl, { stockNumber: stockNumber }, function(res) {
+		if (!res || !res.locations || res.locations.length === 0) {
+			$hint.text(res && res.locHint ? res.locHint : '');
+			return;
+		}
+		var defaultLoc = res.defaultLocNumber || '';
+		$.each(res.locations, function(i, loc) {
+			var label = loc.locNumber + (loc.locName ? ' ' + loc.locName : '');
+			var selected = (loc.locNumber === defaultLoc) ? ' selected' : '';
+			$select.append('<option value="' + loc.locNumber + '"' + selected + '>' + label + '</option>');
+		});
+		$hint.text(res.locHint || '');
+	}, 'json');
+}
+
+function initStockSelection() {
+	var row = getSelectedRow();
+	if (row && row.stockNumber) {
+		$('#srcStockNumber').val(row.stockNumber);
+	}
+	if (defaultSrcStock && $('#srcStockNumber option[value="' + defaultSrcStock + '"]').length) {
+		if (!$('#srcStockNumber').val()) {
+			$('#srcStockNumber').val(defaultSrcStock);
+		}
+	}
+	loadStockLoc($('#srcStockNumber').val(), $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无仓位")}');
+	loadStockLoc($('#destStockNumber').val(), $('#destStockLocNumber'), $('#destLocHint'), '${text("无仓位")}');
+}
+
+$(document).on('change', 'input[name="instockRow"]', function() {
+	var row = getSelectedRow();
+	if (row) {
+		$('#transferQty').attr('max', row.maxQty).val(row.maxQty);
+		if (row.stockNumber) {
+			$('#srcStockNumber').val(row.stockNumber);
+			loadStockLoc(row.stockNumber, $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无仓位")}');
+		}
+	}
+});
+
+$('#srcStockNumber').on('change', function() {
+	loadStockLoc($(this).val(), $('#srcStockLocNumber'), $('#srcLocHint'), '${text("无仓位")}');
+});
+$('#destStockNumber').on('change', function() {
+	loadStockLoc($(this).val(), $('#destStockLocNumber'), $('#destLocHint'), '${text("无仓位")}');
+});
+
+$('#btnSubmitTransfer').on('click', function() {
+	var row = getSelectedRow();
+	if (!row) {
+		js.showMessage('${text("请选择一行生产入库明细")}');
+		return;
+	}
+	var qty = parseFloat($('#transferQty').val());
+	if (!qty || qty <= 0) {
+		js.showMessage('${text("请填写大于 0 的调拨数量")}');
+		return;
+	}
+	if (row.maxQty > 0 && qty > row.maxQty) {
+		js.showMessage('${text("调拨数量不能超过入库数量")} ' + row.maxQty);
+		return;
+	}
+	var srcStock = $.trim($('#srcStockNumber').val());
+	var destStock = $.trim($('#destStockNumber').val());
+	if (!srcStock) {
+		js.showMessage('${text("请选择调出仓库")}');
+		return;
+	}
+	if (!destStock) {
+		js.showMessage('${text("请选择调入仓库")}');
+		return;
+	}
+	if (srcStock === destStock) {
+		js.showMessage('${text("调入仓库不能与调出仓库相同")}');
+		return;
+	}
+	var payload = {
+		moBillNo: moBillNo,
+		instockEntryId: row.entryId,
+		instockBillNo: row.billNo,
+		qty: qty,
+		srcStockNumber: srcStock,
+		srcStockLocNumber: $.trim($('#srcStockLocNumber').val()),
+		destStockNumber: destStock,
+		destStockLocNumber: $.trim($('#destStockLocNumber').val())
+	};
+	$('#btnSubmitTransfer').prop('disabled', true);
+	$.ajax({
+		url: submitUrl,
+		type: 'POST',
+		contentType: 'application/json; charset=utf-8',
+		data: JSON.stringify(payload),
+		success: function(res) {
+			$('#btnSubmitTransfer').prop('disabled', false);
+			if (res && res.result === 'true') {
+				js.showMessage(res.message, null, 'success');
+			} else {
+				js.showMessage(res && res.message ? res.message : '${text("提交失败")}');
+			}
+		},
+		error: function() {
+			$('#btnSubmitTransfer').prop('disabled', false);
+			js.showMessage('${text("请求失败,请检查网络或 ERP 连接")}');
+		}
+	});
+});
+
+$(function() {
+	if ($('input[name="instockRow"]').length === 1) {
+		$('input[name="instockRow"]').prop('checked', true).trigger('change');
+	}
+	initStockSelection();
+});
+</script>

+ 84 - 0
src/main/resources/views/modules/mes/erpTransferDirectList.html

@@ -0,0 +1,84 @@
+<% layout('/layouts/default.html', {title: 'ERP直接调拨单查询', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa fa-exchange"></i> ${text('ERP直接调拨单查询')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpTransferDirect/selectOrder" class="btn btn-primary btnTool" title="${text('增加直接调拨单')}"><i class="fa fa-plus"></i> ${text('增加')}</a>
+				<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('仅展示物料')} <strong>8.10.HEV.10.047696(HEV)</strong> ${text('与')}
+				<strong>8.35.115.10.5003146600021(115)</strong> ${text('的直接调拨明细分录,并按当前组织过滤。')}
+			</div>
+			<#form:form id="searchForm" model="${erpTransferDirect}" action="${ctx}/mes/erpTransferDirect/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="billNoQuery" maxlength="100" class="form-control width-180"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('物料编码')}:</label>
+					<div class="control-inline">
+						<select name="materialNumberQuery" id="materialNumberQuery" class="form-control width-260">
+							<option value="">${text('全部(HEV+115)')}</option>
+							<option value="8.10.HEV.10.047696">8.10.HEV.10.047696(HEV)</option>
+							<option value="8.35.115.10.5003146600021">8.35.115.10.5003146600021(115)</option>
+						</select>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('单据状态')}:</label>
+					<div class="control-inline">
+						<select name="documentStatusQuery" id="documentStatusQuery" class="form-control">
+							<option value="">${text('全部')}</option>
+							<option value="A">${text('创建')}</option>
+							<option value="B">${text('审核中')}</option>
+							<option value="C">${text('已审核')}</option>
+							<option value="D">${text('重新审核')}</option>
+						</select>
+					</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:'billNo', index:'billNo', width:170, align:"center", frozen:true, formatter: function(val){
+			return '<a href="${ctx}/mes/erpTransferDirect/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP直接调拨单明细")}">'+val+'</a>';
+		}},
+		{header:'${text("调拨日期")}', name:'date', index:'date', width:100, align:"center"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"},
+		{header:'${text("单据类型")}', name:'billTypeName', index:'billTypeName', width:100, align:"center"},
+		{header:'${text("库存组织")}', name:'stockOrgName', index:'stockOrgName', width:160, align:"left"},
+		{header:'${text("行号")}', name:'seq', index:'seq', width:60, align:"center"},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:200, align:"left"},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:120, align:"left"},
+		{header:'${text("调拨数量")}', name:'qty', index:'qty', width:90, align:"right", formatter: formatErpQty},
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:60, align:"center"},
+		{header:'${text("调出仓库")}', name:'srcStockName', index:'srcStockName', width:120, align:"left"},
+		{header:'${text("调入仓库")}', name:'destStockName', index:'destStockName', width:140, align:"left"},
+		{header:'${text("批号")}', name:'lotNumber', index:'lotNumber', width:120, align:"left"},
+		{header:'${text("备注")}', name:'note', index:'note', width:160, align:"left"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 57 - 0
src/main/resources/views/modules/mes/erpTransferDirectSelectOrder.html

@@ -0,0 +1,57 @@
+<% 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="${ctx}/mes/erpTransferDirect/list" class="btn btn-default" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+				<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('请选择生产订单,系统将加载该订单 HEV/115 生产入库行作为调拨批号来源。')}
+			</div>
+			<#form:form id="searchForm" model="${erpProductionOrder}" action="${ctx}/mes/erpTransferDirect/selectOrderData" 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="billNo" maxlength="100" class="form-control width-180"/>
+					</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:'actions', width:100, align:"center", frozen:true, formatter: function(val, obj, row){
+			return '<a href="${ctx}/mes/erpTransferDirect/form?moBillNo='+encodeURIComponent(row.billNo)+'" class="btnList addTabPage" data-title="${text("新增直接调拨单")}">'+
+				'<i class="fa fa-check"></i> ${text("选择")}</a>';
+		}},
+		{header:'${text("生产订单号")}', name:'billNo', index:'billNo', width:180, align:"center", frozen:true},
+		{header:'${text("单据日期")}', name:'date', index:'date', width:120, align:"center"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:100, align:"center"},
+		{header:'${text("生产组织")}', name:'prdOrgName', index:'prdOrgName', width:180, align:"left"},
+		{header:'${text("存货类别")}', name:'inventoryCategory', index:'inventoryCategory', width:120, align:"center"},
+		{header:'${text("生产车间")}', name:'workshopName', index:'workshopName', width:140, align:"left"},
+		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:100, align:"right"},
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right", formatter: formatErpQty}
+	],
+	ajaxSuccess: function(){}
+});
+</script>