Browse Source

ERP对接,采购入库功能

jingbo 4 months ago
parent
commit
fc4cd6eb73
34 changed files with 4006 additions and 25 deletions
  1. 289 0
      src/main/java/com/jeesite/modules/mes/erp/ErpInStock.java
  2. 48 0
      src/main/java/com/jeesite/modules/mes/erp/ErpInStockResult.java
  3. 369 0
      src/main/java/com/jeesite/modules/mes/erp/ErpInStockSaveService.java
  4. 378 0
      src/main/java/com/jeesite/modules/mes/erp/ErpInStockService.java
  5. 87 0
      src/main/java/com/jeesite/modules/mes/erp/ErpInStockSubmitRequest.java
  6. 15 0
      src/main/java/com/jeesite/modules/mes/erp/ErpKingdeeParseUtils.java
  7. 105 0
      src/main/java/com/jeesite/modules/mes/erp/ErpLotMasterService.java
  8. 1 1
      src/main/java/com/jeesite/modules/mes/erp/ErpPickMaterialBillService.java
  9. 67 4
      src/main/java/com/jeesite/modules/mes/erp/ErpProductionMaterialService.java
  10. 1 1
      src/main/java/com/jeesite/modules/mes/erp/ErpProductionOrderService.java
  11. 120 0
      src/main/java/com/jeesite/modules/mes/erp/ErpPurchaseOrder.java
  12. 18 4
      src/main/java/com/jeesite/modules/mes/erp/ErpPurchaseOrderService.java
  13. 50 0
      src/main/java/com/jeesite/modules/mes/erp/ErpStockLocOption.java
  14. 308 0
      src/main/java/com/jeesite/modules/mes/erp/ErpStockLocService.java
  15. 40 0
      src/main/java/com/jeesite/modules/mes/erp/ErpStockOption.java
  16. 103 0
      src/main/java/com/jeesite/modules/mes/erp/ErpStockService.java
  17. 114 0
      src/main/java/com/jeesite/modules/mes/erp/KingdeeErpProperties.java
  18. 486 0
      src/main/java/com/jeesite/modules/mes/web/ErpInStockController.java
  19. 15 0
      src/main/resources/config/application.yml
  20. 17 2
      src/main/resources/static/common/common.js
  21. 45 0
      src/main/resources/views/modules/mes/erpInStockDetail.html
  22. 936 0
      src/main/resources/views/modules/mes/erpInStockInspect.html
  23. 88 0
      src/main/resources/views/modules/mes/erpInStockList.html
  24. 136 0
      src/main/resources/views/modules/mes/erpInStockSelectDetail.html
  25. 157 0
      src/main/resources/views/modules/mes/erpInStockSelectOrder.html
  26. 2 2
      src/main/resources/views/modules/mes/erpPickMaterialBillDetail.html
  27. 2 2
      src/main/resources/views/modules/mes/erpPickMaterialBillList.html
  28. 3 3
      src/main/resources/views/modules/mes/erpPickMaterialBillMaterial.html
  29. 1 1
      src/main/resources/views/modules/mes/erpPickMaterialBillSelectDetail.html
  30. 1 1
      src/main/resources/views/modules/mes/erpPickMaterialBillSelectOrder.html
  31. 1 1
      src/main/resources/views/modules/mes/erpProductionOrderDetail.html
  32. 1 1
      src/main/resources/views/modules/mes/erpProductionOrderList.html
  33. 1 1
      src/main/resources/views/modules/mes/erpPurchaseOrderDetail.html
  34. 1 1
      src/main/resources/views/modules/mes/erpPurchaseOrderList.html

+ 289 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpInStock.java

@@ -0,0 +1,289 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * ERP 采购入库单(STK_InStock)BillQuery 数据。
+ */
+public class ErpInStock 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 businessType;
+	private String businessTypeName;
+	private String stockOrgNumber;
+	private String stockOrgName;
+	private String supplierNumber;
+	private String supplierName;
+	private String entryId;
+	private String seq;
+	private String materialNumber;
+	private String materialName;
+	private String unitName;
+	private BigDecimal realQty;
+	private String poOrderNo;
+	private String poEntryId;
+	private String lotNumber;
+	private String stockNumber;
+	private String stockName;
+
+	/** 查询:入库单号(精确) */
+	private String billNoQuery;
+	/** 查询:采购订单号(精确) */
+	private String poOrderNoQuery;
+	/** 查询:供应商编码(模糊) */
+	private String supplierNumberQuery;
+	/** 查询:物料编码(模糊) */
+	private String materialNumberQuery;
+	/** 查询:单据状态 A/B/C/D,空则不过滤 */
+	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 getBusinessType() {
+		return businessType;
+	}
+
+	public void setBusinessType(String businessType) {
+		this.businessType = businessType;
+	}
+
+	public String getBusinessTypeName() {
+		return businessTypeName;
+	}
+
+	public void setBusinessTypeName(String businessTypeName) {
+		this.businessTypeName = businessTypeName;
+	}
+
+	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 getSupplierNumber() {
+		return supplierNumber;
+	}
+
+	public void setSupplierNumber(String supplierNumber) {
+		this.supplierNumber = supplierNumber;
+	}
+
+	public String getSupplierName() {
+		return supplierName;
+	}
+
+	public void setSupplierName(String supplierName) {
+		this.supplierName = supplierName;
+	}
+
+	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 getRealQty() {
+		return realQty;
+	}
+
+	public void setRealQty(BigDecimal realQty) {
+		this.realQty = realQty;
+	}
+
+	public String getPoOrderNo() {
+		return poOrderNo;
+	}
+
+	public void setPoOrderNo(String poOrderNo) {
+		this.poOrderNo = poOrderNo;
+	}
+
+	public String getPoEntryId() {
+		return poEntryId;
+	}
+
+	public void setPoEntryId(String poEntryId) {
+		this.poEntryId = poEntryId;
+	}
+
+	public String getLotNumber() {
+		return lotNumber;
+	}
+
+	public void setLotNumber(String lotNumber) {
+		this.lotNumber = lotNumber;
+	}
+
+	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 getBillNoQuery() {
+		return billNoQuery;
+	}
+
+	public void setBillNoQuery(String billNoQuery) {
+		this.billNoQuery = billNoQuery;
+	}
+
+	public String getPoOrderNoQuery() {
+		return poOrderNoQuery;
+	}
+
+	public void setPoOrderNoQuery(String poOrderNoQuery) {
+		this.poOrderNoQuery = poOrderNoQuery;
+	}
+
+	public String getSupplierNumberQuery() {
+		return supplierNumberQuery;
+	}
+
+	public void setSupplierNumberQuery(String supplierNumberQuery) {
+		this.supplierNumberQuery = supplierNumberQuery;
+	}
+
+	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/ErpInStockResult.java

@@ -0,0 +1,48 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+
+/**
+ * ERP 采购入库单 Save 结果。
+ */
+public class ErpInStockResult implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private boolean success;
+	private String inStockBillNo;
+	private String inStockBillId;
+	private String message;
+
+	public static ErpInStockResult success(String inStockBillNo, String inStockBillId) {
+		ErpInStockResult result = new ErpInStockResult();
+		result.success = true;
+		result.inStockBillNo = inStockBillNo;
+		result.inStockBillId = inStockBillId;
+		result.message = "采购入库单保存成功";
+		return result;
+	}
+
+	public static ErpInStockResult fail(String message) {
+		ErpInStockResult result = new ErpInStockResult();
+		result.success = false;
+		result.message = message;
+		return result;
+	}
+
+	public boolean isSuccess() {
+		return success;
+	}
+
+	public String getInStockBillNo() {
+		return inStockBillNo;
+	}
+
+	public String getInStockBillId() {
+		return inStockBillId;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+}

+ 369 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpInStockSaveService.java

@@ -0,0 +1,369 @@
+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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ERP 采购入库单 Save(STK_InStock + FInStockEntry_Link 关联采购订单)。
+ */
+@Service
+public class ErpInStockSaveService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpInStockSaveService.class);
+	private static final String FORM_ID = "STK_InStock";
+	private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+	private final ErpInStockService erpInStockService;
+	private final ErpProductionMaterialService erpProductionMaterialService;
+	private final ErpStockLocService erpStockLocService;
+
+	public ErpInStockSaveService(KingdeeErpClient erpClient,
+			KingdeeErpProperties erpProperties,
+			ErpInStockService erpInStockService,
+			ErpProductionMaterialService erpProductionMaterialService,
+			ErpStockLocService erpStockLocService) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+		this.erpInStockService = erpInStockService;
+		this.erpProductionMaterialService = erpProductionMaterialService;
+		this.erpStockLocService = erpStockLocService;
+	}
+
+	public ErpInStockResult submit(ErpInStockSubmitRequest request) {
+		if (request == null || StringUtils.isBlank(request.getBillNo())) {
+			return ErpInStockResult.fail("采购订单号不能为空");
+		}
+		if (request.getLines() == null || request.getLines().isEmpty()) {
+			return ErpInStockResult.fail("请至少提交一行入库明细");
+		}
+
+		String poBillNo = request.getBillNo().trim();
+		Map<String, ErpPurchaseOrder> pendingMap = new HashMap<>();
+		for (ErpPurchaseOrder detail : erpInStockService.findPendingPoDetailList(poBillNo)) {
+			pendingMap.put(detail.getEntryId(), detail);
+		}
+		if (pendingMap.isEmpty()) {
+			return ErpInStockResult.fail("采购订单 " + poBillNo + " 已无剩余可入分录");
+		}
+		erpInStockService.enrichInspectLotInfo(new ArrayList<>(pendingMap.values()));
+
+		List<InStockLineContext> inStockLines = new ArrayList<>();
+		for (ErpInStockSubmitRequest.Line line : request.getLines()) {
+			if (line == null || StringUtils.isBlank(line.getEntryId())) {
+				return ErpInStockResult.fail("分录内码不能为空");
+			}
+			ErpPurchaseOrder detail = pendingMap.get(line.getEntryId().trim());
+			if (detail == null) {
+				return ErpInStockResult.fail("分录 " + line.getEntryId() + " 已无剩余可入数量");
+			}
+			BigDecimal inspectQty = ErpKingdeeParseUtils.normalizeQtyNullable(line.getInspectQty());
+			if (inspectQty == null || inspectQty.compareTo(BigDecimal.ZERO) <= 0) {
+				return ErpInStockResult.fail("行 " + detail.getSeq() + " 检验数量必须大于 0");
+			}
+			BigDecimal remainQty = detail.getRemainQty() == null ? BigDecimal.ZERO : detail.getRemainQty();
+			if (inspectQty.compareTo(remainQty) > 0) {
+				return ErpInStockResult.fail("行 " + detail.getSeq() + " 检验数量不能超过剩余可入数量 "
+						+ remainQty.stripTrailingZeros().toPlainString());
+			}
+			String lotNumber = StringUtils.trim(line.getLotNumber());
+			if (detail.isBatchManage() && StringUtils.isBlank(lotNumber)) {
+				return ErpInStockResult.fail("行 " + detail.getSeq() + " 启用批号管理,请从 ERP 选择或手工录入批号");
+			}
+			String validateMsg = validateDetail(detail, inspectQty);
+			if (StringUtils.isNotBlank(validateMsg)) {
+				return ErpInStockResult.fail(validateMsg);
+			}
+			inStockLines.add(new InStockLineContext(detail, inspectQty, lotNumber));
+		}
+
+		if (inStockLines.isEmpty()) {
+			return ErpInStockResult.fail("请填写大于 0 的检验数量");
+		}
+
+		String stockNumber = StringUtils.trim(request.getStockNumber());
+		if (StringUtils.isBlank(stockNumber)) {
+			return ErpInStockResult.fail("请选择入库仓库");
+		}
+		String stockLocNumber = firstNonBlank(request.getStockLocNumber(), erpProperties.getInStockStockLocNumber());
+		if (erpStockLocService.requiresStockLocation(stockNumber) && StringUtils.isBlank(stockLocNumber)) {
+			return ErpInStockResult.fail("所选仓库需要选择仓位");
+		}
+
+		JSONObject saveData = buildSaveData(poBillNo, stockNumber, stockLocNumber, inStockLines);
+		logger.info("ERP in-stock save request: {}", saveData.toJSONString());
+		JSONObject response = erpClient.save(FORM_ID, saveData);
+		return parseSaveResponse(response);
+	}
+
+	private String validateDetail(ErpPurchaseOrder detail, BigDecimal inspectQty) {
+		if (StringUtils.isBlank(detail.getMaterialNumber())) {
+			return "行 " + detail.getSeq() + " 物料编码为空,无法入库";
+		}
+		if (StringUtils.isBlank(detail.getSupplierNumber())) {
+			return "采购订单供应商编码为空,无法入库";
+		}
+		if (StringUtils.isBlank(detail.getFid())) {
+			return "采购订单内码为空,无法关联入库";
+		}
+		if (StringUtils.isBlank(detail.getEntryId())) {
+			return "行 " + detail.getSeq() + " 分录内码为空,无法关联入库";
+		}
+		String unitNumber = resolveUnitNumber(detail);
+		if (StringUtils.isBlank(unitNumber)) {
+			return "行 " + detail.getSeq() + " 单位编码为空,无法入库";
+		}
+		return "";
+	}
+
+	private String resolveUnitNumber(ErpPurchaseOrder detail) {
+		if (StringUtils.isNotBlank(detail.getUnitNumber())) {
+			return detail.getUnitNumber().trim();
+		}
+		return erpProductionMaterialService.findMaterialUnitNumber(detail.getMaterialNumber());
+	}
+
+	private JSONObject buildSaveData(String poBillNo, String stockNumber, String stockLocNumber,
+			List<InStockLineContext> inStockLines) {
+		ErpPurchaseOrder sample = inStockLines.get(0).detail;
+		String orgNumber = firstNonBlank(erpProperties.getOrgNumber(), "110");
+		String supplierNumber = sample.getSupplierNumber().trim();
+
+		JSONObject model = new JSONObject(true);
+		model.put("FID", 0);
+		model.put("FBillTypeID", numberObject(erpProperties.getInStockBillType(), true));
+		model.put("FBusinessType", erpProperties.getInStockBusinessType());
+		model.put("FDate", LocalDate.now().format(DATE_FORMAT));
+		model.put("FStockOrgId", numberObject(orgNumber, false));
+		model.put("FPurchaseOrgId", numberObject(orgNumber, false));
+		model.put("FSupplierId", numberObject(supplierNumber, false));
+		model.put("FOwnerTypeIdHead", erpProperties.getInStockOwnerType());
+		model.put("FOwnerIdHead", numberObject(orgNumber, false));
+		model.put("FInStockFin", buildFin(orgNumber, supplierNumber));
+
+		JSONArray entryList = new JSONArray();
+		for (InStockLineContext lineContext : inStockLines) {
+			entryList.add(buildEntry(poBillNo, stockNumber, stockLocNumber, lineContext, orgNumber));
+		}
+		model.put("FInStockEntry", 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", resolveIsEntryBatchFill(inStockLines));
+		saveData.put("ValidateFlag", "true");
+		saveData.put("NumberSearch", "true");
+		saveData.put("IsAutoAdjustField", "true");
+		saveData.put("IgnoreInterationFlag", "true");
+		saveData.put("Model", model);
+		return saveData;
+	}
+
+	private JSONObject buildFin(String orgNumber, String supplierNumber) {
+		JSONObject fin = new JSONObject(true);
+		fin.put("FSettleOrgId", numberObject(orgNumber, false));
+		fin.put("FSettleCurrId", numberObject(erpProperties.getInStockSettleCurrency(), true));
+		fin.put("FLocalCurrId", numberObject(erpProperties.getInStockSettleCurrency(), true));
+		fin.put("FPriceTimePoint", erpProperties.getInStockPriceTimePoint());
+		fin.put("FSupplyId", numberObject(supplierNumber, false));
+		fin.put("FSettleId", numberObject(supplierNumber, false));
+		fin.put("FChargeId", numberObject(supplierNumber, false));
+		return fin;
+	}
+
+	private JSONObject buildEntry(String poBillNo, String stockNumber, String stockLocNumber,
+			InStockLineContext lineContext, String orgNumber) {
+		ErpPurchaseOrder detail = lineContext.detail;
+		BigDecimal qty = ErpKingdeeParseUtils.normalizeQty(lineContext.inspectQty);
+		Object erpQty = ErpKingdeeParseUtils.toErpQty(qty);
+		String unitNumber = resolveUnitNumber(detail);
+		String stockStatus = erpProperties.getInStockStockStatus();
+		String ownerType = erpProperties.getInStockOwnerType();
+		String keeperType = erpProperties.getInStockKeeperType();
+
+		JSONObject entry = new JSONObject(true);
+		entry.put("FEntryID", 0);
+		entry.put("FMaterialId", numberObject(detail.getMaterialNumber(), false));
+		entry.put("FUnitID", numberObject(unitNumber, false));
+		entry.put("FRealQty", erpQty);
+		entry.put("FMustQty", erpQty);
+		if (detail.getPrice() != null) {
+			entry.put("FPrice", ErpKingdeeParseUtils.toErpQty(detail.getPrice()));
+		}
+		if (detail.getTaxPrice() != null) {
+			entry.put("FTaxPrice", ErpKingdeeParseUtils.toErpQty(detail.getTaxPrice()));
+		}
+		if (detail.getEntryTaxRate() != null) {
+			entry.put("FEntryTaxRate", ErpKingdeeParseUtils.toErpQty(detail.getEntryTaxRate()));
+		}
+		entry.put("FStockId", numberObject(stockNumber, false));
+		entry.put("FStockStatusId", numberObject(stockStatus, false));
+		entry.put("FOwnerTypeId", ownerType);
+		entry.put("FOwnerId", numberObject(orgNumber, false));
+		entry.put("FKeeperTypeId", keeperType);
+		entry.put("FKeeperId", numberObject(orgNumber, false));
+		putStockLoc(entry, stockNumber, stockLocNumber);
+		if (StringUtils.isNotBlank(lineContext.lotNumber)) {
+			entry.put("FLot", numberObject(lineContext.lotNumber, false));
+		}
+		entry.put("FPOOrderNo", poBillNo);
+		entry.put("FPOORDERENTRYID", parseLong(detail.getEntryId()));
+		entry.put("FSrcBillType", "PUR_PurchaseOrder");
+		entry.put("FSrcBillNo", poBillNo);
+		if (StringUtils.isNotBlank(detail.getSeq())) {
+			entry.put("FPOOrderEntrySeq", detail.getSeq());
+		}
+		entry.put("FInStockEntry_Link", buildEntryLink(detail, qty));
+		return entry;
+	}
+
+	private void putStockLoc(JSONObject entry, String stockNumber, String stockLocNumber) {
+		if (!erpStockLocService.requiresStockLocation(stockNumber)) {
+			return;
+		}
+		String locNumber = StringUtils.trim(stockLocNumber);
+		if (StringUtils.isBlank(locNumber)) {
+			locNumber = StringUtils.trim(erpProperties.getInStockStockLocNumber());
+		}
+		if (StringUtils.isBlank(locNumber)) {
+			return;
+		}
+		String locKey = erpStockLocService.resolveLocKeyForStock(stockNumber);
+		if (StringUtils.isBlank(locKey)) {
+			locKey = erpProperties.getInStockStockLocKey();
+		}
+		String saveKey = erpProperties.toStockLocSaveKey(locKey);
+		JSONObject flexValue = numberObject(locNumber, false);
+		JSONObject stockLoc = new JSONObject(true);
+		stockLoc.put(saveKey, flexValue);
+		if (StringUtils.isNotBlank(locKey) && !locKey.equals(saveKey)) {
+			stockLoc.put(locKey, flexValue);
+		}
+		entry.put("FStockLocId", stockLoc);
+	}
+
+	private JSONArray buildEntryLink(ErpPurchaseOrder detail, BigDecimal qty) {
+		Object erpQty = ErpKingdeeParseUtils.toErpQty(qty);
+		JSONObject link = new JSONObject(true);
+		link.put("FInStockEntry_Link_FRuleId", erpProperties.getInStockLinkRule());
+		link.put("FInStockEntry_Link_FSTableName", erpProperties.getInStockLinkTable());
+		link.put("FInStockEntry_Link_FSBillId", parseLong(detail.getFid()));
+		link.put("FInStockEntry_Link_FSId", parseLong(detail.getEntryId()));
+		link.put("FInStockEntry_Link_FBaseUnitQtyOld", erpQty);
+		link.put("FInStockEntry_Link_FBaseUnitQty", erpQty);
+		link.put("FInStockEntry_Link_FRemainInStockBaseQtyOld", erpQty);
+		link.put("FInStockEntry_Link_FRemainInStockBaseQty", erpQty);
+		JSONArray links = new JSONArray();
+		links.add(link);
+		return links;
+	}
+
+	private String resolveIsEntryBatchFill(List<InStockLineContext> inStockLines) {
+		boolean hasNonBatchLine = false;
+		int batchLineCount = 0;
+		for (InStockLineContext lineContext : inStockLines) {
+			if (lineContext.detail.isBatchManage()) {
+				batchLineCount++;
+			} else {
+				hasNonBatchLine = true;
+			}
+		}
+		if (hasNonBatchLine) {
+			return "true";
+		}
+		if (batchLineCount > 1) {
+			return "false";
+		}
+		return "true";
+	}
+
+	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 String firstNonBlank(String... values) {
+		for (String value : values) {
+			if (StringUtils.isNotBlank(value)) {
+				return value.trim();
+			}
+		}
+		return "";
+	}
+
+	private ErpInStockResult parseSaveResponse(JSONObject response) {
+		if (response == null) {
+			return ErpInStockResult.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 ErpInStockResult.success(billNo, billId);
+		}
+		return ErpInStockResult.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();
+	}
+
+	private static class InStockLineContext {
+		private final ErpPurchaseOrder detail;
+		private final BigDecimal inspectQty;
+		private final String lotNumber;
+
+		private InStockLineContext(ErpPurchaseOrder detail, BigDecimal inspectQty, String lotNumber) {
+			this.detail = detail;
+			this.inspectQty = inspectQty;
+			this.lotNumber = lotNumber;
+		}
+	}
+}

+ 378 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpInStockService.java

@@ -0,0 +1,378 @@
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ERP 采购入库单 BillQuery(STK_InStock),默认标准采购入库、收料组织过滤。
+ */
+@Service
+public class ErpInStockService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpInStockService.class);
+	private static final String FORM_ID = "STK_InStock";
+	private static final String BILL_TYPE_FIELD = "FBillTypeID.FNUMBER";
+	private static final String SUPPLIER_NUMBER_FIELD = "FSupplierId.FNUMBER";
+	private static final String MATERIAL_NUMBER_FIELD = "FMaterialId.FNUMBER";
+	private static final String PO_ORDER_NO_FIELD = "FPOOrderNo";
+	private static final String PO_ENTRY_ID_FIELD = "FPOORDERENTRYID";
+	private static final String DETAIL_FIELD_KEYS = String.join(",",
+			"FID",
+			"FBILLNO",
+			"FDate",
+			"FDocumentStatus",
+			BILL_TYPE_FIELD,
+			"FBillTypeID.FNAME",
+			"FBusinessType",
+			"FStockOrgId.FNUMBER",
+			"FStockOrgId.FNAME",
+			SUPPLIER_NUMBER_FIELD,
+			"FSupplierId.FNAME",
+			"FInStockEntry_FEntryID",
+			"FInStockEntry_FSeq",
+			MATERIAL_NUMBER_FIELD,
+			"FMaterialName",
+			"FUnitID.FNAME",
+			"FRealQty",
+			PO_ORDER_NO_FIELD,
+			PO_ENTRY_ID_FIELD,
+			"FLot.FNUMBER",
+			"FStockId.FNUMBER",
+			"FStockId.FNAME");
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+	private final ErpPurchaseOrderService erpPurchaseOrderService;
+	private final ErpProductionMaterialService erpProductionMaterialService;
+	private final ErpLotMasterService erpLotMasterService;
+
+	public ErpInStockService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties,
+			ErpPurchaseOrderService erpPurchaseOrderService,
+			ErpProductionMaterialService erpProductionMaterialService,
+			ErpLotMasterService erpLotMasterService) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+		this.erpPurchaseOrderService = erpPurchaseOrderService;
+		this.erpProductionMaterialService = erpProductionMaterialService;
+		this.erpLotMasterService = erpLotMasterService;
+	}
+
+	/**
+	 * 列表:按入库明细分录返回(与 ERP 采购入库列表一致,一行一条分录)。
+	 */
+	public List<ErpInStock> findPageList(ErpInStock query, int startRow, int limit) {
+		return findDetailList(query, startRow, limit);
+	}
+
+	public List<ErpInStock> findDetailList(String billNo) {
+		if (StringUtils.isBlank(billNo)) {
+			return new ArrayList<>();
+		}
+		ErpInStock query = new ErpInStock();
+		query.setBillNoQuery(billNo.trim());
+		return findDetailList(query, 0, 2000);
+	}
+
+	/**
+	 * 增加入库:选采购订单明细(仅返回剩余可入数量 &gt; 0 的分录)。
+	 */
+	public List<ErpPurchaseOrder> findPendingPoDetailList(String poBillNo) {
+		if (StringUtils.isBlank(poBillNo)) {
+			return new ArrayList<>();
+		}
+		Map<String, BigDecimal> instockMap = sumAuditedInStockQtyByPoEntry(poBillNo.trim());
+		List<ErpPurchaseOrder> details = erpPurchaseOrderService.findDetailList(poBillNo.trim());
+		List<ErpPurchaseOrder> pending = new ArrayList<>();
+		for (ErpPurchaseOrder detail : details) {
+			BigDecimal poQty = ErpKingdeeParseUtils.normalizeQty(detail.getQty());
+			BigDecimal inQty = ErpKingdeeParseUtils.normalizeQty(instockMap.getOrDefault(detail.getEntryId(), BigDecimal.ZERO));
+			detail.setInstockQty(inQty);
+			detail.setRemainQty(ErpKingdeeParseUtils.normalizeQty(poQty.subtract(inQty)));
+			if (detail.getRemainQty().compareTo(BigDecimal.ZERO) > 0) {
+				pending.add(detail);
+			}
+		}
+		return pending;
+	}
+
+	public ErpPurchaseOrder findPendingPoDetail(String poBillNo, String entryId) {
+		if (StringUtils.isBlank(poBillNo) || StringUtils.isBlank(entryId)) {
+			return null;
+		}
+		for (ErpPurchaseOrder detail : findPendingPoDetailList(poBillNo)) {
+			if (entryId.equals(detail.getEntryId())) {
+				return detail;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 按分录内码批量取待入库明细(保持 entryIds 顺序,跳过已无剩余的分录)。
+	 */
+	public List<ErpPurchaseOrder> findPendingPoDetails(String poBillNo, List<String> entryIds) {
+		List<ErpPurchaseOrder> result = new ArrayList<>();
+		if (StringUtils.isBlank(poBillNo) || entryIds == null || entryIds.isEmpty()) {
+			return result;
+		}
+		Set<String> idSet = new LinkedHashSet<>();
+		for (String entryId : entryIds) {
+			if (StringUtils.isNotBlank(entryId)) {
+				idSet.add(entryId.trim());
+			}
+		}
+		if (idSet.isEmpty()) {
+			return result;
+		}
+		Map<String, ErpPurchaseOrder> pendingMap = new HashMap<>();
+		for (ErpPurchaseOrder detail : findPendingPoDetailList(poBillNo.trim())) {
+			pendingMap.put(detail.getEntryId(), detail);
+		}
+		for (String entryId : idSet) {
+			ErpPurchaseOrder detail = pendingMap.get(entryId);
+			if (detail != null) {
+				result.add(detail);
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * 来料检页:从 ERP 物料主数据确认批号管理,并加载批号档案选项。
+	 */
+	public void enrichInspectLotInfo(List<ErpPurchaseOrder> details) {
+		if (details == null || details.isEmpty()) {
+			return;
+		}
+		Set<String> materialNumbers = new LinkedHashSet<>();
+		for (ErpPurchaseOrder detail : details) {
+			if (StringUtils.isNotBlank(detail.getMaterialNumber())) {
+				materialNumbers.add(detail.getMaterialNumber().trim());
+			}
+		}
+		Map<String, Boolean> batchManageMap = erpProductionMaterialService.queryMaterialBatchManageMap(materialNumbers);
+		for (ErpPurchaseOrder detail : details) {
+			detail.setAvailableLots(new ArrayList<>());
+			detail.setLotHint("");
+			detail.setSuggestedLotNumber("");
+			detail.setLotNumber("");
+			String materialNumber = detail.getMaterialNumber();
+			if (StringUtils.isBlank(materialNumber)) {
+				detail.setBatchManage(false);
+				continue;
+			}
+			boolean batchManage = Boolean.TRUE.equals(batchManageMap.get(materialNumber.trim()));
+			if (!batchManage) {
+				batchManage = erpProductionMaterialService.isMaterialBatchManage(materialNumber.trim());
+			}
+			detail.setBatchManage(batchManage);
+			if (!batchManage) {
+				continue;
+			}
+			List<ErpInventoryLotOption> lots = erpLotMasterService.findLotsByMaterial(materialNumber.trim(), null);
+			detail.setAvailableLots(lots);
+			if (lots != null && !lots.isEmpty()) {
+				detail.setSuggestedLotNumber(lots.get(0).getLotNumber());
+				detail.setLotNumber(lots.get(0).getLotNumber());
+			}
+			detail.setLotHint(erpLotMasterService.buildInStockLotHint(lots, detail.getSuggestedLotNumber()));
+		}
+	}
+
+	/**
+	 * 按采购订单分录汇总已审核入库实收数量。
+	 */
+	public Map<String, BigDecimal> sumAuditedInStockQtyByPoEntry(String poBillNo) {
+		Map<String, BigDecimal> qtyMap = new HashMap<>();
+		if (StringUtils.isBlank(poBillNo)) {
+			return qtyMap;
+		}
+		ErpInStock query = new ErpInStock();
+		query.setPoOrderNoQuery(poBillNo.trim());
+		query.setDocumentStatusQuery("C");
+		List<ErpInStock> lines = findDetailList(query, 0, 5000);
+		for (ErpInStock line : lines) {
+			if (StringUtils.isBlank(line.getPoEntryId()) || line.getRealQty() == null) {
+				continue;
+			}
+			qtyMap.merge(line.getPoEntryId(), line.getRealQty(), BigDecimal::add);
+		}
+		return qtyMap;
+	}
+
+	private List<ErpInStock> findDetailList(ErpInStock 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,FInStockEntry_FSeq");
+		data.put("StartRow", Math.max(startRow, 0));
+		data.put("Limit", Math.max(limit, 1));
+		logger.info("ERP in-stock detail query: {}", data.toJSONString());
+		return mapRows(erpClient.billQuery(data));
+	}
+
+	private List<ErpInStock> mapRows(JSONArray rows) {
+		List<ErpInStock> list = new ArrayList<>();
+		for (int i = 0; i < rows.size(); i++) {
+			list.add(toDetail(rows.getJSONObject(i), i));
+		}
+		return list;
+	}
+
+	private ErpInStock toDetail(JSONObject row, int index) {
+		ErpInStock item = new ErpInStock();
+		item.setFid(ErpKingdeeParseUtils.readString(row, "FID"));
+		item.setBillNo(ErpKingdeeParseUtils.readString(row, "FBILLNO", "FBillNo"));
+		item.setEntryId(ErpKingdeeParseUtils.readString(row,
+				"FInStockEntry_FEntryID", "FInStockEntry_FEntryId", "FEntryID", "FEntryId"));
+		item.setSeq(ErpKingdeeParseUtils.readString(row, "FInStockEntry_FSeq", "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(ErpKingdeeParseUtils.readString(row, "FDocumentStatus"));
+		item.setDocumentStatusName(documentStatusName(item.getDocumentStatus()));
+		item.setDate(formatErpDate(ErpKingdeeParseUtils.readString(row, "FDate")));
+		item.setBillTypeNumber(ErpKingdeeParseUtils.readString(row, BILL_TYPE_FIELD, "FBillTypeID.FNumber"));
+		item.setBillTypeName(ErpKingdeeParseUtils.readString(row, "FBillTypeID.FNAME", "FBillTypeID.FName"));
+		item.setBusinessType(ErpKingdeeParseUtils.readString(row, "FBusinessType"));
+		item.setBusinessTypeName(businessTypeName(item.getBusinessType()));
+		item.setStockOrgNumber(ErpKingdeeParseUtils.readString(row, "FStockOrgId.FNUMBER", "FStockOrgId.FNumber"));
+		item.setStockOrgName(ErpKingdeeParseUtils.readString(row, "FStockOrgId.FNAME", "FStockOrgId.FName"));
+		item.setSupplierNumber(ErpKingdeeParseUtils.readString(row, SUPPLIER_NUMBER_FIELD, "FSupplierId.FNumber"));
+		item.setSupplierName(ErpKingdeeParseUtils.readString(row, "FSupplierId.FNAME", "FSupplierId.FName"));
+		item.setMaterialNumber(ErpKingdeeParseUtils.readString(row, MATERIAL_NUMBER_FIELD, "FMaterialId.FNumber"));
+		item.setMaterialName(ErpKingdeeParseUtils.readString(row, "FMaterialName", "FMaterialId.FNAME", "FMaterialId.FName"));
+		item.setUnitName(ErpKingdeeParseUtils.readString(row, "FUnitID.FNAME", "FUnitId.FNAME", "FUnitId.FName"));
+		item.setRealQty(firstBigDecimal(row, "FRealQty"));
+		item.setPoOrderNo(ErpKingdeeParseUtils.readString(row, PO_ORDER_NO_FIELD, "FPOOrderNo", "POOrderNo"));
+		item.setPoEntryId(ErpKingdeeParseUtils.readString(row, PO_ENTRY_ID_FIELD, "FPOORDERENTRYID", "FPOOrderEntryId"));
+		item.setLotNumber(ErpKingdeeParseUtils.readString(row, "FLot.FNUMBER", "FLot.FNumber", "FLot_Text", "FLot"));
+		item.setStockNumber(ErpKingdeeParseUtils.readString(row, "FStockId.FNUMBER", "FStockId.FNumber"));
+		item.setStockName(ErpKingdeeParseUtils.readString(row, "FStockId.FNAME", "FStockId.FName"));
+		return item;
+	}
+
+	private String buildFilterString(ErpInStock query) {
+		String filter = buildBaseFilter();
+		if (query == null) {
+			return filter;
+		}
+		if (StringUtils.isNotBlank(query.getBillNoQuery())) {
+			filter = appendAnd(filter, "FBILLNO='" + escape(query.getBillNoQuery()) + "'");
+		}
+		if (StringUtils.isNotBlank(query.getBillNo())) {
+			filter = appendAnd(filter, "FBILLNO='" + escape(query.getBillNo()) + "'");
+		}
+		if (StringUtils.isNotBlank(query.getPoOrderNoQuery())) {
+			filter = appendAnd(filter, PO_ORDER_NO_FIELD + "='" + escape(query.getPoOrderNoQuery()) + "'");
+		}
+		if (StringUtils.isNotBlank(query.getSupplierNumberQuery())) {
+			filter = appendAnd(filter,
+					SUPPLIER_NUMBER_FIELD + " like '%" + escape(query.getSupplierNumberQuery()) + "%'");
+		}
+		if (StringUtils.isNotBlank(query.getMaterialNumberQuery())) {
+			filter = appendAnd(filter,
+					MATERIAL_NUMBER_FIELD + " like '%" + escape(query.getMaterialNumberQuery()) + "%'");
+		}
+		if (StringUtils.isNotBlank(query.getDocumentStatusQuery())) {
+			filter = appendAnd(filter, "FDocumentStatus='" + escape(query.getDocumentStatusQuery()) + "'");
+		}
+		return filter;
+	}
+
+	private String buildBaseFilter() {
+		String filter = BILL_TYPE_FIELD + "='" + escape(erpProperties.getInStockBillType()) + "'";
+		filter = appendAnd(filter, "FBusinessType='CG'");
+		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 BigDecimal firstBigDecimal(JSONObject row, String... keys) {
+		for (String key : keys) {
+			BigDecimal value = row.getBigDecimal(key);
+			if (value != null) {
+				return ErpKingdeeParseUtils.normalizeQtyNullable(value);
+			}
+		}
+		return null;
+	}
+
+	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;
+	}
+
+	private String businessTypeName(String businessType) {
+		if ("CG".equals(businessType)) {
+			return "标准采购";
+		}
+		if ("WW".equals(businessType)) {
+			return "标准委外";
+		}
+		return businessType;
+	}
+}

+ 87 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpInStockSubmitRequest.java

@@ -0,0 +1,87 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 提交 ERP 采购入库单请求(一张入库单可含多行 PO 分录)。
+ */
+public class ErpInStockSubmitRequest implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String billNo;
+	/** 入库仓库编码(BD_STOCK.FNumber) */
+	private String stockNumber;
+	/** 入库仓位编码(启用仓位管理的仓库必填) */
+	private String stockLocNumber;
+	private List<Line> lines = new ArrayList<>();
+
+	public String getBillNo() {
+		return billNo;
+	}
+
+	public void setBillNo(String billNo) {
+		this.billNo = billNo;
+	}
+
+	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 List<Line> getLines() {
+		return lines;
+	}
+
+	public void setLines(List<Line> lines) {
+		this.lines = lines;
+	}
+
+	public static class Line implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		private String entryId;
+		private BigDecimal inspectQty;
+		/** 批号(启用批号管理的物料必填) */
+		private String lotNumber;
+
+		public String getEntryId() {
+			return entryId;
+		}
+
+		public void setEntryId(String entryId) {
+			this.entryId = entryId;
+		}
+
+		public BigDecimal getInspectQty() {
+			return inspectQty;
+		}
+
+		public void setInspectQty(BigDecimal inspectQty) {
+			this.inspectQty = inspectQty;
+		}
+
+		public String getLotNumber() {
+			return lotNumber;
+		}
+
+		public void setLotNumber(String lotNumber) {
+			this.lotNumber = lotNumber;
+		}
+	}
+}

+ 15 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpKingdeeParseUtils.java

@@ -119,6 +119,21 @@ public final class ErpKingdeeParseUtils {
 		return qty.setScale(10, RoundingMode.HALF_UP).stripTrailingZeros();
 	}
 
+	public static BigDecimal normalizeQtyNullable(BigDecimal qty) {
+		if (qty == null) {
+			return null;
+		}
+		return normalizeQty(qty);
+	}
+
+	/** 页面展示用:去掉 ERP 返回的多余小数位。 */
+	public static String formatDisplayQty(BigDecimal qty) {
+		if (qty == null) {
+			return "0";
+		}
+		return normalizeQty(qty).toPlainString();
+	}
+
 	/** 与金蝶 Save 接口兼容的数量序列化:整数用 long,避免 2400 与 2400.0 不一致。 */
 	public static Object toErpQty(BigDecimal qty) {
 		BigDecimal normalized = normalizeQty(qty);

+ 105 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpLotMasterService.java

@@ -0,0 +1,105 @@
+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 批号主档(BD_BatchMainFile),供采购入库来料检选择批号。
+ * <p>
+ * 采购入库为新增库存,批号来自批号档案或现场手工录入,不应查即时库存(STK_Inventory)。
+ */
+@Service
+public class ErpLotMasterService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpLotMasterService.class);
+	private static final String FORM_ID = "BD_BatchMainFile";
+	private static final String MATERIAL_NUMBER_FIELD = "FMaterialId.FNumber";
+	private static final String FIELD_KEYS = String.join(",",
+			"FNumber",
+			MATERIAL_NUMBER_FIELD,
+			"FMaterialId.FName");
+
+	private final KingdeeErpClient erpClient;
+
+	public ErpLotMasterService(KingdeeErpClient erpClient) {
+		this.erpClient = erpClient;
+	}
+
+	/**
+	 * 按物料编码查询 ERP 批号档案(可选批号关键字)。
+	 */
+	public List<ErpInventoryLotOption> findLotsByMaterial(String materialNumber, String lotKeyword) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return new ArrayList<>();
+		}
+		String filter = MATERIAL_NUMBER_FIELD + "='" + escape(materialNumber.trim()) + "'";
+		if (StringUtils.isNotBlank(lotKeyword)) {
+			filter = filter + " AND FNumber like '%" + escapeLike(lotKeyword.trim()) + "%'";
+		}
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", FIELD_KEYS);
+		data.put("FilterString", filter);
+		data.put("OrderString", "FNumber DESC");
+		data.put("StartRow", 0);
+		data.put("Limit", 200);
+		logger.info("ERP lot master query: {}", data.toJSONString());
+		try {
+			return mapRows(erpClient.billQuery(data), materialNumber.trim());
+		} catch (RuntimeException ex) {
+			logger.warn("ERP lot master query failed, material={}, msg={}", materialNumber, ex.getMessage());
+			return new ArrayList<>();
+		}
+	}
+
+	public String buildInStockLotHint(List<ErpInventoryLotOption> lots, String selectedLot) {
+		if (lots == null || lots.isEmpty()) {
+			return "ERP 无历史批号档案,请手工录入到货批号";
+		}
+		if (StringUtils.isNotBlank(selectedLot)) {
+			return "已选批号 " + selectedLot + ",共 " + lots.size() + " 个档案可选,也可改手工录入";
+		}
+		return "共 " + lots.size() + " 个 ERP 批号档案,请点击「选批号」或手工录入新批号";
+	}
+
+	private List<ErpInventoryLotOption> mapRows(JSONArray rows, String expectedMaterial) {
+		Map<String, ErpInventoryLotOption> lotMap = new LinkedHashMap<>();
+		for (int i = 0; i < rows.size(); i++) {
+			JSONObject row = rows.getJSONObject(i);
+			String lotNumber = ErpKingdeeParseUtils.readString(row, "FNumber", "FBILLNO", "FBillNo");
+			String materialNumber = ErpKingdeeParseUtils.readString(row, MATERIAL_NUMBER_FIELD, "FMaterialId.FNumber");
+			if (StringUtils.isBlank(lotNumber)) {
+				continue;
+			}
+			if (StringUtils.isNotBlank(expectedMaterial)
+					&& StringUtils.isNotBlank(materialNumber)
+					&& !expectedMaterial.equals(materialNumber.trim())) {
+				continue;
+			}
+			ErpInventoryLotOption option = lotMap.get(lotNumber);
+			if (option == null) {
+				option = new ErpInventoryLotOption();
+				option.setLotNumber(lotNumber.trim());
+				lotMap.put(lotNumber.trim(), option);
+			}
+		}
+		return new ArrayList<>(lotMap.values());
+	}
+
+	private String escape(String value) {
+		return value == null ? "" : value.trim().replace("'", "''");
+	}
+
+	private String escapeLike(String value) {
+		return escape(value).replace("[", "[[]").replace("%", "[%]").replace("_", "[_]");
+	}
+}

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

@@ -438,7 +438,7 @@ public class ErpPickMaterialBillService {
 		for (String key : keys) {
 			BigDecimal value = row.getBigDecimal(key);
 			if (value != null) {
-				return value;
+				return ErpKingdeeParseUtils.normalizeQtyNullable(value);
 			}
 		}
 		return null;

+ 67 - 4
src/main/java/com/jeesite/modules/mes/erp/ErpProductionMaterialService.java

@@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -201,10 +203,10 @@ public class ErpProductionMaterialService {
 		material.setMaterialNumber(getNumber(materialObj));
 		material.setMaterialName(getNameValue(materialObj));
 		material.setSpecification(getSpecification(materialObj));
-		material.setMustQty(row.getBigDecimal("MustQty"));
-		material.setPickedQty(row.getBigDecimal("PickedQty"));
-		material.setNoPickedQty(row.getBigDecimal("NoPickedQty"));
-		material.setIssueQty(row.getBigDecimal("IssueQty"));
+		material.setMustQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("MustQty")));
+		material.setPickedQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("PickedQty")));
+		material.setNoPickedQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("NoPickedQty")));
+		material.setIssueQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("IssueQty")));
 		material.setUnitName(getNameValue(row.getJSONObject("UnitID")));
 		material.setUnitNumber(getNumber(row.getJSONObject("UnitID")));
 		material.setBaseUnitNumber(getNumber(firstObject(row, "BaseUnitID", "BaseUnitId")));
@@ -382,6 +384,67 @@ public class ErpProductionMaterialService {
 		}
 	}
 
+	/**
+	 * 批量查询物料是否启用批号管理(BillQuery BD_Material)。
+	 */
+	public Map<String, Boolean> queryMaterialBatchManageMap(Collection<String> materialNumbers) {
+		if (materialNumbers == null || materialNumbers.isEmpty()) {
+			return new LinkedHashMap<>();
+		}
+		Set<String> numbers = new LinkedHashSet<>();
+		for (String materialNumber : materialNumbers) {
+			if (StringUtils.isNotBlank(materialNumber)) {
+				numbers.add(materialNumber.trim());
+			}
+		}
+		if (numbers.isEmpty()) {
+			return new LinkedHashMap<>();
+		}
+		return queryMaterialBatchManage(numbers);
+	}
+
+	/**
+	 * 单个物料是否启用批号管理(BillQuery 未命中时回退 View)。
+	 */
+	public boolean isMaterialBatchManage(String materialNumber) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return false;
+		}
+		Map<String, Boolean> map = queryMaterialBatchManageMap(Collections.singleton(materialNumber.trim()));
+		Boolean batchManage = map.get(materialNumber.trim());
+		if (Boolean.TRUE.equals(batchManage)) {
+			return true;
+		}
+		return queryViewBatchManage(materialNumber.trim());
+	}
+
+	/**
+	 * 查询物料计量单位编码(BillQuery BD_Material,PO 未带单位时回退)。
+	 */
+	public String findMaterialUnitNumber(String materialNumber) {
+		if (StringUtils.isBlank(materialNumber)) {
+			return "";
+		}
+		JSONObject data = new JSONObject();
+		data.put("FormId", MATERIAL_FORM_ID);
+		data.put("FieldKeys", "FNumber,FUnitID.FNUMBER");
+		data.put("FilterString", "FNumber='" + escape(materialNumber.trim()) + "'");
+		data.put("OrderString", "FNumber");
+		data.put("StartRow", 0);
+		data.put("Limit", 1);
+		try {
+			JSONArray rows = erpClient.billQuery(data);
+			if (rows == null || rows.isEmpty()) {
+				return "";
+			}
+			return ErpKingdeeParseUtils.readBillQueryString(rows.get(0), 1,
+					"FUnitID.FNUMBER", "FUnitId.FNUMBER", "FUnitId.FNumber");
+		} catch (RuntimeException e) {
+			logger.warn("ERP material unit BillQuery failed, materialNumber={}: {}", materialNumber, e.getMessage());
+			return "";
+		}
+	}
+
 	private Map<String, Boolean> queryMaterialBatchManage(Set<String> materialNumbers) {
 		Map<String, Boolean> result = new LinkedHashMap<>();
 		StringBuilder filter = new StringBuilder();

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

@@ -414,7 +414,7 @@ public class ErpProductionOrderService {
 		order.setMaterialNumber(row.getString("FMATERIALID.FNUMBER"));
 		order.setMaterialName(row.getString("FMATERIALID.FNAME"));
 		order.setSpecification(readSpecification(row));
-		order.setQty(row.getBigDecimal("FQTY"));
+		order.setQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FQTY")));
 		order.setUnitName(row.getString("FUnitId.FNAME"));
 		order.setPlanStartDate(formatErpDate(row.getString("FPlanStartDate")));
 		order.setPlanFinishDate(formatErpDate(row.getString("FPlanFinishDate")));

+ 120 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpPurchaseOrder.java

@@ -2,6 +2,7 @@ package com.jeesite.modules.mes.erp;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.util.List;
 
 /**
  * ERP 采购订单(PUR_PurchaseOrder)BillQuery 数据。
@@ -30,16 +31,39 @@ public class ErpPurchaseOrder implements Serializable {
 	private String materialNumber;
 	private String materialName;
 	private String unitName;
+	private String unitNumber;
 	private BigDecimal qty;
 	private String deliveryDate;
 	private BigDecimal price;
+	private BigDecimal taxPrice;
+	private BigDecimal entryTaxRate;
 	private BigDecimal amount;
 	private String sourceBillNo;
 
 	/** 查询条件:供应商编码(模糊) */
 	private String supplierNumberQuery;
+	/** 查询条件:供应商名称(模糊) */
+	private String supplierNameQuery;
 	/** 查询条件:物料编码(模糊) */
 	private String materialNumberQuery;
+	/** 查询条件:单据状态(如 C=已审核) */
+	private String documentStatusQuery;
+
+	/** 已审核入库合计(增加入库时计算) */
+	private BigDecimal instockQty;
+	/** 剩余可入数量(增加入库时计算) */
+	private BigDecimal remainQty;
+
+	/** 是否启用批号管理(ERP 物料主数据) */
+	private boolean batchManage;
+	/** ERP 批号档案选项 */
+	private List<ErpInventoryLotOption> availableLots;
+	/** 推荐批号 */
+	private String suggestedLotNumber;
+	/** 批号说明 */
+	private String lotHint;
+	/** 已选/录入批号 */
+	private String lotNumber;
 
 	public String getId() {
 		return id;
@@ -201,6 +225,14 @@ public class ErpPurchaseOrder implements Serializable {
 		this.unitName = unitName;
 	}
 
+	public String getUnitNumber() {
+		return unitNumber;
+	}
+
+	public void setUnitNumber(String unitNumber) {
+		this.unitNumber = unitNumber;
+	}
+
 	public BigDecimal getQty() {
 		return qty;
 	}
@@ -225,6 +257,22 @@ public class ErpPurchaseOrder implements Serializable {
 		this.price = price;
 	}
 
+	public BigDecimal getTaxPrice() {
+		return taxPrice;
+	}
+
+	public void setTaxPrice(BigDecimal taxPrice) {
+		this.taxPrice = taxPrice;
+	}
+
+	public BigDecimal getEntryTaxRate() {
+		return entryTaxRate;
+	}
+
+	public void setEntryTaxRate(BigDecimal entryTaxRate) {
+		this.entryTaxRate = entryTaxRate;
+	}
+
 	public BigDecimal getAmount() {
 		return amount;
 	}
@@ -249,6 +297,14 @@ public class ErpPurchaseOrder implements Serializable {
 		this.supplierNumberQuery = supplierNumberQuery;
 	}
 
+	public String getSupplierNameQuery() {
+		return supplierNameQuery;
+	}
+
+	public void setSupplierNameQuery(String supplierNameQuery) {
+		this.supplierNameQuery = supplierNameQuery;
+	}
+
 	public String getMaterialNumberQuery() {
 		return materialNumberQuery;
 	}
@@ -256,4 +312,68 @@ public class ErpPurchaseOrder implements Serializable {
 	public void setMaterialNumberQuery(String materialNumberQuery) {
 		this.materialNumberQuery = materialNumberQuery;
 	}
+
+	public String getDocumentStatusQuery() {
+		return documentStatusQuery;
+	}
+
+	public void setDocumentStatusQuery(String documentStatusQuery) {
+		this.documentStatusQuery = documentStatusQuery;
+	}
+
+	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;
+	}
+
+	public boolean isBatchManage() {
+		return batchManage;
+	}
+
+	public void setBatchManage(boolean batchManage) {
+		this.batchManage = batchManage;
+	}
+
+	public List<ErpInventoryLotOption> getAvailableLots() {
+		return availableLots;
+	}
+
+	public void setAvailableLots(List<ErpInventoryLotOption> availableLots) {
+		this.availableLots = availableLots;
+	}
+
+	public String getSuggestedLotNumber() {
+		return suggestedLotNumber;
+	}
+
+	public void setSuggestedLotNumber(String suggestedLotNumber) {
+		this.suggestedLotNumber = suggestedLotNumber;
+	}
+
+	public String getLotHint() {
+		return lotHint;
+	}
+
+	public void setLotHint(String lotHint) {
+		this.lotHint = lotHint;
+	}
+
+	public String getLotNumber() {
+		return lotNumber;
+	}
+
+	public void setLotNumber(String lotNumber) {
+		this.lotNumber = lotNumber;
+	}
 }

+ 18 - 4
src/main/java/com/jeesite/modules/mes/erp/ErpPurchaseOrderService.java

@@ -24,6 +24,7 @@ public class ErpPurchaseOrderService {
 	private static final String BILL_TYPE_FIELD = "FBillTypeID.FNUMBER";
 	private static final String MATERIAL_NUMBER_FIELD = "FMaterialId.FNUMBER";
 	private static final String SUPPLIER_NUMBER_FIELD = "FSupplierId.FNUMBER";
+	private static final String SUPPLIER_NAME_FIELD = "FSupplierId.FNAME";
 	private static final String HEADER_FIELD_KEYS = String.join(",",
 			"FID",
 			"FBILLNO",
@@ -52,9 +53,12 @@ public class ErpPurchaseOrderService {
 			MATERIAL_NUMBER_FIELD,
 			"FMaterialName",
 			"FUnitID.FNAME",
+			"FUnitID.FNUMBER",
 			"FQty",
 			"FDeliveryDate",
 			"FPrice",
+			"FTaxPrice",
+			"FEntryTaxRate",
 			"FAmount");
 
 	private final KingdeeErpClient erpClient;
@@ -232,10 +236,13 @@ public class ErpPurchaseOrderService {
 		order.setMaterialNumber(readString(row, MATERIAL_NUMBER_FIELD, "FMaterialId.FNumber"));
 		order.setMaterialName(readString(row, "FMaterialName", "FMaterialId.FNAME", "FMaterialId.FName"));
 		order.setUnitName(readString(row, "FUnitID.FNAME", "FUnitId.FNAME", "FUnitId.FName"));
-		order.setQty(row.getBigDecimal("FQty"));
+		order.setUnitNumber(readString(row, "FUnitID.FNUMBER", "FUnitId.FNUMBER", "FUnitId.FNumber"));
+		order.setQty(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FQty")));
 		order.setDeliveryDate(formatErpDate(readString(row, "FDeliveryDate")));
-		order.setPrice(row.getBigDecimal("FPrice"));
-		order.setAmount(row.getBigDecimal("FAmount"));
+		order.setPrice(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FPrice")));
+		order.setTaxPrice(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FTaxPrice")));
+		order.setEntryTaxRate(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FEntryTaxRate")));
+		order.setAmount(ErpKingdeeParseUtils.normalizeQtyNullable(row.getBigDecimal("FAmount")));
 		return order;
 	}
 
@@ -258,12 +265,19 @@ public class ErpPurchaseOrderService {
 			return filter;
 		}
 		if (StringUtils.isNotBlank(query.getBillNo())) {
-			filter = appendAnd(filter, "FBILLNO='" + escape(query.getBillNo()) + "'");
+			filter = appendAnd(filter, "FBILLNO like '%" + escape(query.getBillNo()) + "%'");
 		}
 		if (StringUtils.isNotBlank(query.getSupplierNumberQuery())) {
 			filter = appendAnd(filter,
 					SUPPLIER_NUMBER_FIELD + " like '%" + escape(query.getSupplierNumberQuery()) + "%'");
 		}
+		if (StringUtils.isNotBlank(query.getSupplierNameQuery())) {
+			filter = appendAnd(filter,
+					SUPPLIER_NAME_FIELD + " like '%" + escape(query.getSupplierNameQuery()) + "%'");
+		}
+		if (StringUtils.isNotBlank(query.getDocumentStatusQuery())) {
+			filter = appendAnd(filter, "FDocumentStatus='" + escape(query.getDocumentStatusQuery()) + "'");
+		}
 		if (includeMaterialFilter && StringUtils.isNotBlank(query.getMaterialNumberQuery())) {
 			filter = appendAnd(filter,
 					MATERIAL_NUMBER_FIELD + " like '%" + escape(query.getMaterialNumberQuery()) + "%'");

+ 50 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpStockLocOption.java

@@ -0,0 +1,50 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+
+/**
+ * ERP 仓库仓位选项。
+ */
+public class ErpStockLocOption implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String locNumber;
+	private String locName;
+	/** 仓位维度键(如 F100004),Save 时写入 FStockLocId */
+	private String locKey;
+
+	public String getLocNumber() {
+		return locNumber;
+	}
+
+	public void setLocNumber(String locNumber) {
+		this.locNumber = locNumber;
+	}
+
+	public String getLocName() {
+		return locName;
+	}
+
+	public void setLocName(String locName) {
+		this.locName = locName;
+	}
+
+	public String getLocKey() {
+		return locKey;
+	}
+
+	public void setLocKey(String locKey) {
+		this.locKey = locKey;
+	}
+
+	public String getDisplayLabel() {
+		if (locNumber == null) {
+			return "";
+		}
+		if (locName == null || locName.isEmpty() || locName.trim().equals(locNumber.trim())) {
+			return locNumber;
+		}
+		return locNumber + " " + locName;
+	}
+}

+ 308 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpStockLocService.java

@@ -0,0 +1,308 @@
+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;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 查询金蝶 ERP 仓库仓位:通过 BD_STOCK 仓库档案 + BD_FLEXVALUES 仓位值集明细获取。
+ * 本账套无 BD_StockLoc 表单,不能再用 BillQuery BD_StockLoc。
+ */
+@Service
+public class ErpStockLocService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpStockLocService.class);
+	private static final String STOCK_FORM_ID = "BD_STOCK";
+	private static final String FLEX_FORM_ID = "BD_FLEXVALUES";
+	private static final long CACHE_TTL_MS = 5 * 60 * 1000L;
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+	private final Map<String, CacheEntry> locationCache = new ConcurrentHashMap<>();
+
+	public ErpStockLocService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+	}
+
+	/**
+	 * 判断所选仓库是否需要选择仓位(仓库启用仓位且 ERP 能查到仓位值集明细)。
+	 */
+	public boolean requiresStockLocation(String stockNumber) {
+		return !findLocationsByStock(stockNumber).isEmpty();
+	}
+
+	public List<ErpStockLocOption> findLocationsByStock(String stockNumber) {
+		if (StringUtils.isBlank(stockNumber)) {
+			return new ArrayList<>();
+		}
+		String key = stockNumber.trim();
+		CacheEntry cached = locationCache.get(key);
+		if (cached != null && !cached.isExpired()) {
+			return copyOptions(cached.locations);
+		}
+		List<ErpStockLocOption> locations = loadLocationsFromErp(key);
+		if (locations.isEmpty()) {
+			locations = buildConfiguredFallback(key);
+		}
+		locationCache.put(key, new CacheEntry(copyOptions(locations), System.currentTimeMillis() + CACHE_TTL_MS));
+		return locations;
+	}
+
+	/**
+	 * 解析仓库 Save 时使用的仓位维度键(如 F100004),未启用仓位时返回空。
+	 */
+	public String resolveLocKeyForStock(String stockNumber) {
+		if (StringUtils.isBlank(stockNumber)) {
+			return "";
+		}
+		StockFlexContext flexContext = loadStockFlexContext(stockNumber.trim());
+		if (flexContext == null || StringUtils.isBlank(flexContext.locKey)) {
+			return StringUtils.trim(erpProperties.getInStockStockLocKey());
+		}
+		return flexContext.locKey;
+	}
+
+	public String resolveDefaultLocNumber(String stockNumber, List<ErpStockLocOption> locations) {
+		String configured = erpProperties.getInStockStockLocNumber();
+		if (StringUtils.isNotBlank(configured) && containsLoc(locations, configured.trim())) {
+			return configured.trim();
+		}
+		if (locations != null && !locations.isEmpty()) {
+			return locations.get(0).getLocNumber();
+		}
+		return "";
+	}
+
+	private List<ErpStockLocOption> loadLocationsFromErp(String stockNumber) {
+		StockFlexContext flexContext = loadStockFlexContext(stockNumber);
+		if (flexContext == null || StringUtils.isBlank(flexContext.flexValueSetNumber)) {
+			return new ArrayList<>();
+		}
+		return queryFlexValueEntries(flexContext.flexValueSetNumber, flexContext.locKey);
+	}
+
+	private StockFlexContext loadStockFlexContext(String stockNumber) {
+		try {
+			JSONObject viewResult = viewStock(stockNumber);
+			JSONObject stock = extractViewResult(viewResult);
+			if (stock == null || stock.isEmpty()) {
+				return null;
+			}
+			if (!ErpKingdeeParseUtils.parseBoolean(stock, "IsOpenLocation", "FIsOpenLocation")) {
+				return null;
+			}
+			JSONArray flexItems = stock.getJSONArray("StockFlexItem");
+			if (flexItems == null || flexItems.isEmpty()) {
+				return null;
+			}
+			for (int i = 0; i < flexItems.size(); i++) {
+				JSONObject flexItem = flexItems.getJSONObject(i);
+				if (flexItem == null) {
+					continue;
+				}
+				JSONObject flexId = flexItem.getJSONObject("FlexId");
+				if (flexId == null) {
+					continue;
+				}
+				String valueSetNumber = ErpKingdeeParseUtils.readString(flexId, "Number", "FNumber", "FNUMBER");
+				String flexNumber = ErpKingdeeParseUtils.readString(flexId, "FlexNumber", "FFlexNumber");
+				String locKey = toLocKey(flexNumber);
+				if (StringUtils.isNotBlank(valueSetNumber)) {
+					StockFlexContext context = new StockFlexContext();
+					context.flexValueSetNumber = valueSetNumber.trim();
+					context.locKey = locKey;
+					return context;
+				}
+			}
+		} catch (RuntimeException ex) {
+			logger.warn("ERP stock flex query failed, stockNumber={}: {}", stockNumber, ex.getMessage());
+		}
+		return null;
+	}
+
+	private List<ErpStockLocOption> queryFlexValueEntries(String flexValueSetNumber, String locKey) {
+		try {
+			JSONObject viewResult = viewFlexValueSet(flexValueSetNumber);
+			JSONObject flexValues = extractViewResult(viewResult);
+			if (flexValues == null || flexValues.isEmpty()) {
+				return new ArrayList<>();
+			}
+			JSONArray entries = flexValues.getJSONArray("FLEXVALUESENTRY");
+			if (entries == null || entries.isEmpty()) {
+				entries = flexValues.getJSONArray("FlexValuesEntry");
+			}
+			return mapFlexEntries(entries, locKey);
+		} catch (RuntimeException ex) {
+			logger.warn("ERP flex value query failed, flexValueSetNumber={}: {}", flexValueSetNumber, ex.getMessage());
+			return new ArrayList<>();
+		}
+	}
+
+	private List<ErpStockLocOption> mapFlexEntries(JSONArray entries, String locKey) {
+		Map<String, ErpStockLocOption> locMap = new LinkedHashMap<>();
+		if (entries == null) {
+			return new ArrayList<>();
+		}
+		for (int i = 0; i < entries.size(); i++) {
+			JSONObject entry = entries.getJSONObject(i);
+			if (entry == null || isFlexEntryForbidden(entry)) {
+				continue;
+			}
+			String locNumber = ErpKingdeeParseUtils.readString(entry, "Number", "FNumber", "FNUMBER");
+			if (StringUtils.isBlank(locNumber)) {
+				continue;
+			}
+			ErpStockLocOption option = new ErpStockLocOption();
+			option.setLocNumber(locNumber.trim());
+			option.setLocName(readFlexEntryName(entry));
+			option.setLocKey(locKey);
+			locMap.put(option.getLocNumber(), option);
+		}
+		return new ArrayList<>(locMap.values());
+	}
+
+	private boolean isFlexEntryForbidden(JSONObject entry) {
+		if (ErpKingdeeParseUtils.parseBoolean(entry, "Forbid", "FForbid")) {
+			return true;
+		}
+		String rowStatus = ErpKingdeeParseUtils.readString(entry, "RowlStatus", "FRowlStatus", "DocumentStatus");
+		return StringUtils.isNotBlank(rowStatus) && !"C".equalsIgnoreCase(rowStatus);
+	}
+
+	private String readFlexEntryName(JSONObject entry) {
+		JSONArray names = entry.getJSONArray("Name");
+		if (names != null && !names.isEmpty()) {
+			JSONObject first = names.getJSONObject(0);
+			if (first != null) {
+				String value = ErpKingdeeParseUtils.readString(first, "Value");
+				if (StringUtils.isNotBlank(value)) {
+					return value;
+				}
+			}
+		}
+		JSONArray multiLanguage = entry.getJSONArray("MultiLanguageText");
+		if (multiLanguage != null && !multiLanguage.isEmpty()) {
+			String name = ErpKingdeeParseUtils.readString(multiLanguage.getJSONObject(0), "Name");
+			if (StringUtils.isNotBlank(name)) {
+				return name;
+			}
+		}
+		return ErpKingdeeParseUtils.readString(entry, "Number", "FNumber");
+	}
+
+	private JSONObject viewStock(String stockNumber) {
+		return erpClient.view(STOCK_FORM_ID, buildViewData(stockNumber));
+	}
+
+	private JSONObject viewFlexValueSet(String flexValueSetNumber) {
+		return erpClient.view(FLEX_FORM_ID, buildViewData(flexValueSetNumber));
+	}
+
+	private JSONObject buildViewData(String number) {
+		JSONObject data = new JSONObject();
+		data.put("CreateOrgId", 0);
+		data.put("Number", number);
+		data.put("Id", "");
+		data.put("IsSortBySeq", false);
+		return data;
+	}
+
+	private JSONObject extractViewResult(JSONObject viewResponse) {
+		if (viewResponse == null) {
+			return null;
+		}
+		JSONObject resultWrapper = viewResponse.getJSONObject("Result");
+		if (resultWrapper == null) {
+			return null;
+		}
+		return resultWrapper.getJSONObject("Result");
+	}
+
+	private String toLocKey(String flexNumber) {
+		if (StringUtils.isBlank(flexNumber)) {
+			return "";
+		}
+		String trimmed = flexNumber.trim();
+		if (trimmed.startsWith("FF") && trimmed.length() > 2) {
+			return "F" + trimmed.substring(2);
+		}
+		if (trimmed.startsWith("F")) {
+			return trimmed;
+		}
+		return "F" + trimmed;
+	}
+
+	private List<ErpStockLocOption> buildConfiguredFallback(String stockNumber) {
+		String mappedStock = erpProperties.getInStockStockNumber();
+		if (StringUtils.isBlank(mappedStock) || !mappedStock.trim().equals(stockNumber)) {
+			return new ArrayList<>();
+		}
+		String configured = erpProperties.getInStockStockLocNumber();
+		if (StringUtils.isBlank(configured)) {
+			return new ArrayList<>();
+		}
+		ErpStockLocOption option = new ErpStockLocOption();
+		option.setLocNumber(configured.trim());
+		option.setLocName("默认仓位");
+		option.setLocKey(erpProperties.getInStockStockLocKey());
+		List<ErpStockLocOption> list = new ArrayList<>();
+		list.add(option);
+		return list;
+	}
+
+	private List<ErpStockLocOption> copyOptions(List<ErpStockLocOption> source) {
+		List<ErpStockLocOption> copy = new ArrayList<>();
+		if (source == null) {
+			return copy;
+		}
+		for (ErpStockLocOption item : source) {
+			ErpStockLocOption option = new ErpStockLocOption();
+			option.setLocNumber(item.getLocNumber());
+			option.setLocName(item.getLocName());
+			option.setLocKey(item.getLocKey());
+			copy.add(option);
+		}
+		return copy;
+	}
+
+	private boolean containsLoc(List<ErpStockLocOption> locations, String locNumber) {
+		if (locations == null || StringUtils.isBlank(locNumber)) {
+			return false;
+		}
+		for (ErpStockLocOption location : locations) {
+			if (locNumber.equals(location.getLocNumber())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private static class StockFlexContext {
+		private String flexValueSetNumber;
+		private String locKey;
+	}
+
+	private static class CacheEntry {
+		private final List<ErpStockLocOption> locations;
+		private final long expireAt;
+
+		private CacheEntry(List<ErpStockLocOption> locations, long expireAt) {
+			this.locations = locations;
+			this.expireAt = expireAt;
+		}
+
+		private boolean isExpired() {
+			return System.currentTimeMillis() > expireAt;
+		}
+	}
+}

+ 40 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpStockOption.java

@@ -0,0 +1,40 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+
+/**
+ * ERP 仓库(BD_STOCK)选项。
+ */
+public class ErpStockOption implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String stockNumber;
+	private String stockName;
+
+	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 getDisplayLabel() {
+		if (stockNumber == null) {
+			return "";
+		}
+		if (stockName == null || stockName.isEmpty()) {
+			return stockNumber;
+		}
+		return stockNumber + " " + stockName;
+	}
+}

+ 103 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpStockService.java

@@ -0,0 +1,103 @@
+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 仓库主档(BD_STOCK),供采购入库选择入库仓库。
+ */
+@Service
+public class ErpStockService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpStockService.class);
+	private static final String FORM_ID = "BD_STOCK";
+	private static final String FIELD_KEYS = "FNumber,FName,FUseOrgId.FNUMBER,FDocumentStatus,FForbidStatus";
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+
+	public ErpStockService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+	}
+
+	/**
+	 * 查询当前组织下可用仓库(已审核、未禁用)。
+	 */
+	public List<ErpStockOption> findUsableStockList() {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", FIELD_KEYS);
+		data.put("FilterString", buildFilterString());
+		data.put("OrderString", "FNumber");
+		data.put("StartRow", 0);
+		data.put("Limit", 500);
+		logger.info("ERP stock master query: {}", data.toJSONString());
+		try {
+			return mapRows(erpClient.billQuery(data));
+		} catch (RuntimeException ex) {
+			logger.warn("ERP stock master query failed: {}", ex.getMessage());
+			return new ArrayList<>();
+		}
+	}
+
+	public String resolveDefaultStockNumber(List<ErpStockOption> stockList) {
+		String configured = erpProperties.getInStockStockNumber();
+		if (StringUtils.isNotBlank(configured) && containsStock(stockList, configured.trim())) {
+			return configured.trim();
+		}
+		if (stockList != null && !stockList.isEmpty()) {
+			return stockList.get(0).getStockNumber();
+		}
+		return configured == null ? "" : configured.trim();
+	}
+
+	private boolean containsStock(List<ErpStockOption> stockList, String stockNumber) {
+		if (stockList == null || StringUtils.isBlank(stockNumber)) {
+			return false;
+		}
+		for (ErpStockOption stock : stockList) {
+			if (stockNumber.equals(stock.getStockNumber())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private List<ErpStockOption> mapRows(JSONArray rows) {
+		Map<String, ErpStockOption> stockMap = new LinkedHashMap<>();
+		for (int i = 0; i < rows.size(); i++) {
+			JSONObject row = rows.getJSONObject(i);
+			String stockNumber = ErpKingdeeParseUtils.readString(row, "FNumber", "FNUMBER");
+			if (StringUtils.isBlank(stockNumber)) {
+				continue;
+			}
+			ErpStockOption option = new ErpStockOption();
+			option.setStockNumber(stockNumber.trim());
+			option.setStockName(ErpKingdeeParseUtils.readString(row, "FName", "FNAME"));
+			stockMap.put(option.getStockNumber(), option);
+		}
+		return new ArrayList<>(stockMap.values());
+	}
+
+	private String buildFilterString() {
+		String filter = "FDocumentStatus='C' AND FForbidStatus='A'";
+		if (erpProperties.isFilterCurrentOrg() && StringUtils.isNotBlank(erpProperties.getOrgNumber())) {
+			filter = filter + " AND FUseOrgId.FNUMBER='" + escape(erpProperties.getOrgNumber()) + "'";
+		}
+		return filter;
+	}
+
+	private String escape(String value) {
+		return value == null ? "" : value.trim().replace("'", "''");
+	}
+}

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

@@ -1,5 +1,6 @@
 package com.jeesite.modules.mes.erp;
 
+import com.jeesite.common.lang.StringUtils;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
@@ -67,6 +68,44 @@ public class KingdeeErpProperties {
 	@Value("${erp.k3cloud.purchase-order.bill-type:CGDD01_SYS}")
 	private String purchaseOrderBillType;
 
+	/** 标准采购入库单单据类型编码 */
+	@Value("${erp.k3cloud.in-stock.bill-type:RKD01_SYS}")
+	private String inStockBillType;
+
+	@Value("${erp.k3cloud.in-stock.business-type:CG}")
+	private String inStockBusinessType;
+
+	@Value("${erp.k3cloud.in-stock.stock-number:1100105}")
+	private String inStockStockNumber;
+
+	@Value("${erp.k3cloud.in-stock.stock-status:KCZT01_SYS}")
+	private String inStockStockStatus;
+
+	@Value("${erp.k3cloud.in-stock.owner-type:BD_OwnerOrg}")
+	private String inStockOwnerType;
+
+	@Value("${erp.k3cloud.in-stock.keeper-type:BD_KeeperOrg}")
+	private String inStockKeeperType;
+
+	@Value("${erp.k3cloud.in-stock.link-rule:PUR_PurchaseOrder-STK_InStock}")
+	private String inStockLinkRule;
+
+	@Value("${erp.k3cloud.in-stock.link-table:t_PUR_POOrderEntry}")
+	private String inStockLinkTable;
+
+	@Value("${erp.k3cloud.in-stock.settle-currency:PRE001}")
+	private String inStockSettleCurrency;
+
+	@Value("${erp.k3cloud.in-stock.price-time-point:1}")
+	private String inStockPriceTimePoint;
+
+	/** 五金库仓位:FStockLocId.F100004 */
+	@Value("${erp.k3cloud.in-stock.stock-loc-key:F100004}")
+	private String inStockStockLocKey;
+
+	@Value("${erp.k3cloud.in-stock.stock-loc-number:}")
+	private String inStockStockLocNumber;
+
 	public String getBaseUrl() {
 		if (baseUrl.endsWith("/")) {
 			return baseUrl.substring(0, baseUrl.length() - 1);
@@ -145,4 +184,79 @@ public class KingdeeErpProperties {
 	public String getPurchaseOrderBillType() {
 		return purchaseOrderBillType;
 	}
+
+	public String getInStockBillType() {
+		return inStockBillType;
+	}
+
+	public String getInStockBusinessType() {
+		return inStockBusinessType;
+	}
+
+	public String getInStockStockNumber() {
+		return inStockStockNumber;
+	}
+
+	public String getInStockStockStatus() {
+		return inStockStockStatus;
+	}
+
+	public String getInStockOwnerType() {
+		return inStockOwnerType;
+	}
+
+	public String getInStockKeeperType() {
+		return inStockKeeperType;
+	}
+
+	public String getInStockLinkRule() {
+		return inStockLinkRule;
+	}
+
+	public String getInStockLinkTable() {
+		return inStockLinkTable;
+	}
+
+	public String getInStockSettleCurrency() {
+		return inStockSettleCurrency;
+	}
+
+	public String getInStockPriceTimePoint() {
+		return inStockPriceTimePoint;
+	}
+
+	public String getInStockStockLocKey() {
+		return inStockStockLocKey;
+	}
+
+	public String getInStockStockLocNumber() {
+		return inStockStockLocNumber;
+	}
+
+	/**
+	 * 判断所选仓库是否需要填写仓位(当前按配置仓库 + 仓位维度键判断)。
+	 */
+	public boolean requiresStockLocation(String stockNumber) {
+		if (StringUtils.isBlank(stockNumber) || StringUtils.isBlank(inStockStockLocKey)) {
+			return false;
+		}
+		return StringUtils.isNotBlank(inStockStockNumber) && inStockStockNumber.trim().equals(stockNumber.trim());
+	}
+
+	/**
+	 * 仓位维度在 Save 接口中的字段名(F100004 → FSTOCKLOCID__FF100004)。
+	 */
+	public String toStockLocSaveKey(String locKey) {
+		String key = StringUtils.isBlank(locKey) ? inStockStockLocKey : locKey.trim();
+		if (StringUtils.isBlank(key)) {
+			return "FSTOCKLOCID__FF100004";
+		}
+		if (key.startsWith("FSTOCKLOCID__")) {
+			return key;
+		}
+		if (key.startsWith("F")) {
+			return "FSTOCKLOCID__F" + key;
+		}
+		return "FSTOCKLOCID__FF" + key;
+	}
 }

+ 486 - 0
src/main/java/com/jeesite/modules/mes/web/ErpInStockController.java

@@ -0,0 +1,486 @@
+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.ErpInStock;
+
+import com.jeesite.modules.mes.erp.ErpInStockService;
+
+import com.jeesite.modules.mes.erp.ErpInStockSaveService;
+import com.jeesite.modules.mes.erp.ErpInStockResult;
+import com.jeesite.modules.mes.erp.ErpInStockSubmitRequest;
+
+import com.jeesite.modules.mes.erp.ErpPurchaseOrder;
+
+import com.jeesite.modules.mes.erp.ErpPurchaseOrderService;
+
+import com.jeesite.modules.mes.erp.ErpLotMasterService;
+
+import com.jeesite.modules.mes.erp.ErpStockService;
+
+import com.jeesite.modules.mes.erp.ErpStockOption;
+
+import com.jeesite.modules.mes.erp.ErpStockLocService;
+
+import com.jeesite.modules.mes.erp.ErpStockLocOption;
+
+import com.jeesite.modules.mes.erp.ErpInventoryLotOption;
+
+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.ArrayList;
+
+import java.util.HashMap;
+
+import java.util.List;
+
+import java.util.Map;
+
+
+
+/**
+
+ * ERP 采购入库单查询与增加入库。
+
+ */
+
+@Controller
+
+@RequestMapping(value = "${adminPath}/mes/erpInStock")
+
+public class ErpInStockController extends BaseController {
+
+
+
+	private final ErpInStockService erpInStockService;
+
+	private final ErpPurchaseOrderService erpPurchaseOrderService;
+
+	private final ErpLotMasterService erpLotMasterService;
+
+	private final ErpInStockSaveService erpInStockSaveService;
+
+	private final ErpStockService erpStockService;
+
+	private final ErpStockLocService erpStockLocService;
+
+
+
+	public ErpInStockController(ErpInStockService erpInStockService,
+
+			ErpPurchaseOrderService erpPurchaseOrderService,
+
+			ErpLotMasterService erpLotMasterService,
+
+			ErpInStockSaveService erpInStockSaveService,
+
+			ErpStockService erpStockService,
+
+			ErpStockLocService erpStockLocService) {
+
+		this.erpInStockService = erpInStockService;
+
+		this.erpPurchaseOrderService = erpPurchaseOrderService;
+
+		this.erpLotMasterService = erpLotMasterService;
+
+		this.erpInStockSaveService = erpInStockSaveService;
+
+		this.erpStockService = erpStockService;
+
+		this.erpStockLocService = erpStockLocService;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = {"list", ""})
+
+	public String list(ErpInStock erpInStock, Model model) {
+
+		model.addAttribute("erpInStock", erpInStock);
+
+		return "modules/mes/erpInStockList";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "listData")
+
+	@ResponseBody
+
+	public Page<ErpInStock> listData(ErpInStock erpInStock,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpInStock> 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<ErpInStock> list = erpInStockService.findPageList(erpInStock, startRow, pageSize);
+
+		page.setList(list);
+
+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "detail")
+
+	public String detail(ErpInStock erpInStock, Model model) {
+
+		model.addAttribute("erpInStock", erpInStock);
+
+		return "modules/mes/erpInStockDetail";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "detailData")
+
+	@ResponseBody
+
+	public Page<ErpInStock> detailData(ErpInStock erpInStock,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpInStock> page = new Page<>(request, response);
+
+		List<ErpInStock> list = erpInStockService.findDetailList(erpInStock.getBillNo());
+
+		page.setList(list);
+
+		page.setCount(list.size());
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "selectOrder")
+
+	public String selectOrder(ErpPurchaseOrder erpPurchaseOrder, Model model) {
+
+		if (erpPurchaseOrder == null) {
+			erpPurchaseOrder = new ErpPurchaseOrder();
+		}
+		String docStatus = erpPurchaseOrder.getDocumentStatusQuery();
+		if (StringUtils.isBlank(docStatus)) {
+			docStatus = "C";
+			erpPurchaseOrder.setDocumentStatusQuery(docStatus);
+		}
+		model.addAttribute("erpPurchaseOrder", erpPurchaseOrder);
+		model.addAttribute("docStatus", docStatus);
+
+		return "modules/mes/erpInStockSelectOrder";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "selectOrderData")
+
+	@ResponseBody
+
+	public Page<ErpPurchaseOrder> selectOrderData(ErpPurchaseOrder erpPurchaseOrder,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpPurchaseOrder> 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<ErpPurchaseOrder> list = erpPurchaseOrderService.findPageList(erpPurchaseOrder, startRow, pageSize);
+
+		page.setList(list);
+
+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "selectDetail")
+
+	public String selectDetail(ErpPurchaseOrder erpPurchaseOrder, Model model) {
+
+		model.addAttribute("erpPurchaseOrder", erpPurchaseOrder);
+
+		return "modules/mes/erpInStockSelectDetail";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "selectDetailData")
+
+	@ResponseBody
+
+	public Page<ErpPurchaseOrder> selectDetailData(ErpPurchaseOrder erpPurchaseOrder,
+
+			HttpServletRequest request, HttpServletResponse response) {
+
+		Page<ErpPurchaseOrder> page = new Page<>(request, response);
+
+		List<ErpPurchaseOrder> list = erpInStockService.findPendingPoDetailList(erpPurchaseOrder.getBillNo());
+
+		page.setList(list);
+
+		page.setCount(list.size());
+
+		return page;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@RequestMapping(value = "inspect")
+
+	public String inspect(ErpPurchaseOrder erpPurchaseOrder, String entryIds, Model model) {
+
+		String billNo = erpPurchaseOrder == null ? "" : erpPurchaseOrder.getBillNo();
+
+		List<String> idList = parseEntryIds(entryIds, erpPurchaseOrder == null ? "" : erpPurchaseOrder.getEntryId());
+
+		List<ErpPurchaseOrder> detailList = erpInStockService.findPendingPoDetails(billNo, idList);
+
+		if (detailList.isEmpty()) {
+
+			model.addAttribute("message", "所选分录已无剩余可入数量");
+
+		} else {
+
+			erpInStockService.enrichInspectLotInfo(detailList);
+
+		}
+
+		model.addAttribute("billNo", billNo);
+
+		model.addAttribute("detailList", detailList);
+
+		List<ErpStockOption> stockList = erpStockService.findUsableStockList();
+
+		model.addAttribute("stockList", stockList);
+
+		model.addAttribute("defaultStockNumber", erpStockService.resolveDefaultStockNumber(stockList));
+
+		return "modules/mes/erpInStockInspect";
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock: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() + " 个仓位,数据来自 ERP 仓位值集");
+
+			if (StringUtils.isNotBlank(locations.get(0).getLocKey())) {
+
+				result.put("locKey", locations.get(0).getLocKey());
+
+			}
+
+		}
+
+		return result;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@GetMapping(value = "lotOptions")
+
+	@ResponseBody
+
+	public Map<String, Object> lotOptions(String materialNumber) {
+
+		List<ErpInventoryLotOption> lots = erpLotMasterService.findLotsByMaterial(materialNumber, null);
+
+		String suggested = lots.isEmpty() ? "" : lots.get(0).getLotNumber();
+
+		Map<String, Object> result = new HashMap<>(4);
+
+		result.put("lots", lots);
+
+		result.put("suggestedLotNumber", suggested);
+
+		result.put("lotHint", erpLotMasterService.buildInStockLotHint(lots, suggested));
+
+		return result;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@GetMapping(value = "lotSearch")
+
+	@ResponseBody
+
+	public Map<String, Object> lotSearch(String materialNumber, String lotKeyword) {
+
+		List<ErpInventoryLotOption> lots = erpLotMasterService.findLotsByMaterial(materialNumber, lotKeyword);
+
+		Map<String, Object> result = new HashMap<>(3);
+
+		result.put("lots", lots);
+
+		if (lots.isEmpty()) {
+
+			result.put("lotHint", "未查到符合条件的 ERP 批号档案,可手工录入新批号");
+
+		} else {
+
+			result.put("lotHint", "共查到 " + lots.size() + " 条批号档案");
+
+		}
+
+		return result;
+
+	}
+
+
+
+	@RequiresPermissions("mes:erpInStock:view")
+
+	@PostMapping(value = "submitInStock")
+
+	@ResponseBody
+
+	public String submitInStock(@RequestBody ErpInStockSubmitRequest request) {
+
+		ErpInStockResult result = erpInStockSaveService.submit(request);
+
+		if (result.isSuccess()) {
+
+			return renderResult(Global.TRUE, text("采购入库单保存成功,单号:{0}", result.getInStockBillNo()));
+
+		}
+
+		return renderResult(Global.FALSE, result.getMessage());
+
+	}
+
+
+
+	private List<String> parseEntryIds(String entryIds, String singleEntryId) {
+
+		List<String> idList = new ArrayList<>();
+
+		if (StringUtils.isNotBlank(entryIds)) {
+
+			for (String part : entryIds.split(",")) {
+
+				if (StringUtils.isNotBlank(part)) {
+
+					idList.add(part.trim());
+
+				}
+
+			}
+
+		}
+
+		if (idList.isEmpty() && StringUtils.isNotBlank(singleEntryId)) {
+
+			idList.add(singleEntryId.trim());
+
+		}
+
+		return idList;
+
+	}
+
+}
+
+

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

@@ -65,6 +65,21 @@ erp:
     # 采购订单 PUR_PurchaseOrder,标准采购订单单据类型
     purchase-order:
       bill-type: CGDD01_SYS
+    # 采购入库单 STK_InStock Save(Apifox View CGRK 已验证)
+    in-stock:
+      bill-type: RKD01_SYS
+      business-type: CG
+      # 来料检页默认选中的仓库(用户可在页面改选)
+      stock-number: 1100105
+      stock-status: KCZT01_SYS
+      owner-type: BD_OwnerOrg
+      keeper-type: BD_KeeperOrg
+      link-rule: PUR_PurchaseOrder-STK_InStock
+      link-table: t_PUR_POOrderEntry
+      settle-currency: PRE001
+      price-time-point: "1"
+      stock-loc-key: F100004
+      stock-loc-number: A
 
 #======================================#
 #========== Server settings ===========#

+ 17 - 2
src/main/resources/static/common/common.js

@@ -27,7 +27,6 @@ function layer_open(_self,flag){
     });
 }
 
-//打开查看图片
 function open_img(_self) {
     var src = $(_self).attr('src');
     var img = new Image();
@@ -68,4 +67,20 @@ function open_img(_self) {
             content: imgs
         });
     };
-}
+}
+
+/** ERP 数量列展示:去掉多余小数位 */
+function formatErpQty(val) {
+    if (val == null || val === '') {
+        return '';
+    }
+    var num = Number(val);
+    if (isNaN(num)) {
+        return val;
+    }
+    if (Math.floor(num) === num) {
+        return String(num);
+    }
+    var text = num.toFixed(8).replace(/(\.\d*?[1-9])0+$/, '$1').replace(/\.0+$/, '');
+    return text;
+}

+ 45 - 0
src/main/resources/views/modules/mes/erpInStockDetail.html

@@ -0,0 +1,45 @@
+<% 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采购入库单明细')}:${erpInStock.billNo}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpInStock/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="${erpInStock}" action="${ctx}/mes/erpInStock/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:'poOrderNo', index:'poOrderNo', width:170, align:"center", frozen:true, formatter: function(val){
+			if (!val) { return ''; }
+			return '<a href="${ctx}/mes/erpPurchaseOrder/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP采购订单明细")}">'+val+'</a>';
+		}},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:180, align:"left"},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:220, align:"left"},
+		{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:'lotNumber', index:'lotNumber', width:130, align:"center"},
+		{header:'${text("仓库")}', name:'stockName', index:'stockName', width:120, align:"left"},
+		{header:'${text("供应商")}', name:'supplierName', index:'supplierName', width:160, align:"left"},
+		{header:'${text("入库日期")}', name:'date', index:'date', width:110, align:"center"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 936 - 0
src/main/resources/views/modules/mes/erpInStockInspect.html

@@ -0,0 +1,936 @@
+<% 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-check-square-o"></i> ${text('增加采购入库 - 来料检')}:${billNo!}
+
+			</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('检验OK并入库')}</a>
+
+				<a href="${ctx}/mes/erpInStock/selectDetail?billNo=${billNo!}" 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('多行合并一张入库单,请先选择入库仓库;启用仓位的仓库请选择入库仓位。批号管理物料请从 ERP 批号档案选择,也可手工录入到货批号。')}
+
+			</div>
+
+			<div class="row" style="margin-bottom:12px;">
+
+				<div class="col-xs-12 col-sm-6">
+
+					<label class="control-label">${text('采购订单')}:</label>
+
+					<a href="${ctx}/mes/erpPurchaseOrder/detail?billNo=${billNo!}" class="addTabPage">${billNo!}</a>
+
+				</div>
+
+				<div class="col-xs-12 col-sm-6">
+					<label class="control-label">${text('入库行数')}:</label>
+					<span id="lineCount">${detailList.~size}</span>
+				</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 加载仓库列表,请检查组织与 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" id="stockLocWrap">
+
+					<label class="control-label"><span class="required" id="stockLocRequiredMark" style="display:none;">*</span> ${text('入库仓位')}:</label>
+
+					<div class="stock-loc-row">
+						<input type="hidden" id="stockLocNumber" value=""/>
+						<input type="text" id="stockLocDisplay" class="form-control" readonly="readonly"
+							placeholder="${text('请点击右侧按钮选择仓位')}"/>
+						<a href="javascript:void(0)" class="btn btn-primary" id="btnStockLocSearch" style="display:none;">
+							<i class="fa fa-search"></i> ${text('选仓位')}
+						</a>
+					</div>
+
+					<small id="stockLocHint" class="text-muted"></small>
+
+				</div>
+
+			</div>
+
+			<div class="inspect-table-wrap">
+				<table class="table table-bordered table-hover inspect-table" id="inspectTable">
+					<thead>
+					<tr>
+						<th class="col-seq">${text('行号')}</th>
+						<th class="col-material-no">${text('物料编码')}</th>
+						<th class="col-material-name">${text('物料名称')}</th>
+						<th class="col-qty">${text('剩余可入')}</th>
+						<th class="col-qty">${text('本次检验数量')}</th>
+						<th class="col-lot">${text('批号')}</th>
+					</tr>
+					</thead>
+					<tbody>
+					<% for(detail in detailList){ %>
+					<% var lotVal = detail.lotNumber!''; if (isBlank(lotVal)) { lotVal = detail.suggestedLotNumber!''; } %>
+					<% var remainText = @com.jeesite.modules.mes.erp.ErpKingdeeParseUtils.formatDisplayQty(detail.remainQty); %>
+					<tr class="inspect-row"
+						data-entry-id="${detail.entryId!}"
+						data-seq="${detail.seq!}"
+						data-remain="${remainText}"
+						data-batch-manage="${detail.batchManage ? '1' : '0'}"
+						data-material="${detail.materialNumber!}"
+						data-material-name="${detail.materialName!}">
+						<td class="text-center">${detail.seq!}</td>
+						<td>${detail.materialNumber!}</td>
+						<td>${detail.materialName!}</td>
+						<td class="text-right">${remainText} ${detail.unitName!}</td>
+						<td>
+							<input type="number" class="form-control input-sm inspect-qty" value="${remainText}" min="0" step="any"/>
+						</td>
+						<td class="lot-cell">
+							<% if(!detail.batchManage){ %>
+							<span class="text-muted">${text('无需批号')}</span>
+							<% }else{ %>
+							<div class="instock-lot-wrap">
+								<input type="hidden" class="instock-lot-value" value="${lotVal}"/>
+								<div class="instock-lot-row">
+									<a href="javascript:void(0)" class="btn btn-xs btn-primary instock-lot-open"
+										data-entry-id="${detail.entryId!}"
+										data-material="${detail.materialNumber!}"
+										data-material-name="${detail.materialName!}">
+										<i class="fa fa-search"></i> ${text('选批号')}
+									</a>
+									<input type="text" class="form-control input-sm instock-lot-input"
+										value="${lotVal}"
+										placeholder="${text('手工录入新批号')}" maxlength="100"/>
+								</div>
+								<% if(isNotBlank(detail.lotHint!)) { %>
+								<small class="instock-lot-hint" title="${detail.lotHint!}">${detail.lotHint!}</small>
+								<% } %>
+							</div>
+							<% } %>
+						</td>
+					</tr>
+					<% } %>
+					</tbody>
+				</table>
+			</div>
+
+		</div>
+
+	</div>
+
+</div>
+
+<% } %>
+
+<style>
+.inspect-table-wrap {
+	overflow-x: auto;
+	overflow-y: visible;
+	-webkit-overflow-scrolling: touch;
+	margin-bottom: 0;
+}
+.inspect-table {
+	min-width: 1120px;
+	margin-bottom: 0;
+}
+.inspect-table .col-seq { width: 60px; min-width: 60px; }
+.inspect-table .col-material-no { min-width: 160px; }
+.inspect-table .col-material-name { min-width: 140px; white-space: normal; }
+.inspect-table .col-qty { width: 110px; min-width: 110px; white-space: nowrap; }
+.inspect-table .col-lot { min-width: 360px; white-space: normal; }
+.inspect-table .inspect-qty { width: 100%; min-width: 90px; max-width: 110px; }
+.instock-lot-wrap { min-width: 340px; }
+.instock-lot-row { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; }
+.instock-lot-row .instock-lot-open { flex: 0 0 auto; white-space: nowrap; }
+.instock-lot-row .instock-lot-input { flex: 1 1 auto; min-width: 160px; max-width: 240px; }
+.instock-lot-hint {
+	display: block;
+	margin-top: 6px;
+	line-height: 1.4;
+	font-size: 11px;
+	color: #888;
+	max-width: 420px;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+.lot-search-table-wrap { overflow: auto; max-height: 340px; }
+.stock-loc-search-table-wrap { overflow: auto; max-height: 360px; min-height: 240px; -webkit-overflow-scrolling: touch; }
+.stock-loc-row { display: flex; align-items: center; gap: 8px; max-width: 420px; flex-wrap: nowrap; }
+.stock-loc-row #stockLocDisplay { flex: 1 1 auto; min-width: 0; background: #fff; cursor: default; }
+.stock-loc-row #btnStockLocSearch { flex: 0 0 auto; white-space: nowrap; }
+</style>
+
+<script id="lotSearchTpl" type="text/template">//<!--
+
+<div class="form-horizontal" style="padding:12px;">
+
+	<div class="form-group" style="margin-bottom:8px;">
+
+		<label class="control-label col-sm-3">${text('物料编码')}</label>
+
+		<div class="col-sm-9"><p class="form-control-static">{{d.materialNumber}}</p></div>
+
+	</div>
+
+	<div class="form-group" style="margin-bottom:8px;">
+
+		<label class="control-label col-sm-3">${text('物料名称')}</label>
+
+		<div class="col-sm-9"><p class="form-control-static">{{d.materialName}}</p></div>
+
+	</div>
+
+	<div class="form-group" style="margin-bottom:8px;">
+
+		<label class="control-label col-sm-3">${text('批号')}</label>
+
+		<div class="col-sm-6"><input type="text" id="lotKeyword" class="form-control input-sm" placeholder="${text('批号关键字')}"/></div>
+
+		<div class="col-sm-3"><button type="button" id="btnLotSearch" class="btn btn-primary btn-sm btn-block"><i class="fa fa-search"></i> ${text('查询')}</button></div>
+
+	</div>
+
+	<div id="lotSearchHint" class="text-muted" style="margin-bottom:6px;"></div>
+
+	<div id="lotSearchResult"></div>
+
+</div>
+
+//--></script>
+
+<script id="stockLocSearchTpl" type="text/template">//<!--
+
+<div class="form-horizontal" style="padding:12px;">
+
+	<div class="form-group" style="margin-bottom:8px;">
+
+		<label class="control-label col-sm-3">${text('仓位编码')}</label>
+
+		<div class="col-sm-6"><input type="text" id="stockLocKeyword" class="form-control input-sm" placeholder="${text('编码或名称关键字')}"/></div>
+
+		<div class="col-sm-3"><button type="button" id="btnStockLocFilter" class="btn btn-primary btn-sm btn-block"><i class="fa fa-search"></i> ${text('搜索')}</button></div>
+
+	</div>
+
+	<div id="stockLocSearchHint" class="text-muted" style="margin-bottom:6px;"></div>
+
+	<div id="stockLocSearchResult"></div>
+
+</div>
+
+//--></script>
+
+<script>
+
+var lotSearchUrl = '${ctx}/mes/erpInStock/lotSearch';
+
+var stockLocOptionsUrl = '${ctx}/mes/erpInStock/stockLocOptions';
+
+var stockLocRequired = false;
+
+var stockLocOptions = [];
+
+
+
+function formatStockLocLabel(loc) {
+
+	if (!loc) return '';
+
+	var locNo = loc.locNumber || '';
+
+	var locName = loc.locName || '';
+
+	if (loc.displayLabel) {
+
+		return loc.displayLabel;
+
+	}
+
+	if (!locName || locName === locNo) {
+
+		return locNo;
+
+	}
+
+	return locNo + ' ' + locName;
+
+}
+
+
+
+function applyStockLocSelection(loc) {
+
+	if (!loc || !loc.locNumber) return;
+
+	$('#stockLocNumber').val(loc.locNumber);
+
+	$('#stockLocDisplay').val(formatStockLocLabel(loc));
+
+}
+
+
+
+function clearStockLocSelection() {
+
+	$('#stockLocNumber').val('');
+
+	$('#stockLocDisplay').val('');
+
+}
+
+
+
+function filterStockLocOptions(keyword) {
+
+	var kw = $.trim(keyword || '').toLowerCase();
+
+	if (kw === '') {
+
+		return stockLocOptions.slice(0);
+
+	}
+
+	var matched = [];
+
+	for (var i = 0; i < stockLocOptions.length; i++) {
+
+		var loc = stockLocOptions[i];
+
+		var locNo = (loc.locNumber || '').toLowerCase();
+
+		var locName = (loc.locName || '').toLowerCase();
+
+		var label = formatStockLocLabel(loc).toLowerCase();
+
+		if (locNo.indexOf(kw) >= 0 || locName.indexOf(kw) >= 0 || label.indexOf(kw) >= 0) {
+
+			matched.push(loc);
+
+		}
+
+	}
+
+	return matched;
+
+}
+
+
+
+function renderStockLocSearchTable(locations) {
+
+	if (!locations || locations.length === 0) {
+
+		return '<div class="text-muted" style="padding:10px;">${text("未查到符合条件的仓位")}</div>';
+
+	}
+
+	var html = '<div class="stock-loc-search-table-wrap"><table class="table table-bordered table-hover table-condensed"><thead><tr>'+
+
+		'<th style="width:220px;">${text("仓位编码")}</th><th>${text("名称")}</th><th style="width:80px;">${text("操作")}</th></tr></thead><tbody>';
+
+	for (var i = 0; i < locations.length; i++) {
+
+		var loc = locations[i];
+
+		var locNo = loc.locNumber || '';
+
+		var locName = loc.locName || '';
+
+		if (locName === locNo) locName = '';
+
+		html += '<tr><td>'+escapeHtml(locNo)+'</td><td>'+escapeHtml(locName)+'</td><td align="center">'+
+
+			'<button type="button" class="btn btn-xs btn-primary stock-loc-pick" data-loc-number="'+escapeHtml(locNo)+'">${text("选择")}</button></td></tr>';
+
+	}
+
+	html += '</tbody></table></div>';
+
+	return html;
+
+}
+
+
+
+function openStockLocSearchPopup() {
+
+	if (!stockLocRequired || stockLocOptions.length === 0) {
+
+		js.showMessage('${text("当前仓库暂无仓位可选")}');
+
+		return;
+
+	}
+
+	js.layer.open({
+
+		type: 1,
+
+		area: ['640px', '480px'],
+
+		title: '${text("ERP 仓位选择")}',
+
+		resize: false,
+
+		scrollbar: true,
+
+		content: js.template('stockLocSearchTpl', {}),
+
+		success: function(layero, index) {
+
+			layero = $(layero);
+
+			var pickLoc = function(loc) {
+
+				applyStockLocSelection(loc);
+
+				js.layer.close(index);
+
+			};
+
+			var findLocByNumber = function(locNo) {
+
+				if (!locNo) return null;
+
+				for (var i = 0; i < stockLocOptions.length; i++) {
+
+					if (stockLocOptions[i].locNumber === locNo) {
+
+						return stockLocOptions[i];
+
+					}
+
+				}
+
+				return null;
+
+			};
+
+			var renderList = function(list) {
+
+				layero.find('#stockLocSearchHint').text('${text("共")} ' + list.length + ' ${text("条,可滚动浏览列表,也可输入关键字搜索")}');
+
+				layero.find('#stockLocSearchResult').html(renderStockLocSearchTable(list));
+
+			};
+
+			var doSearch = function() {
+
+				renderList(filterStockLocOptions(layero.find('#stockLocKeyword').val()));
+
+			};
+
+			layero.off('click.stockLocPick').on('click.stockLocPick', '.stock-loc-pick', function(e) {
+
+				e.preventDefault();
+
+				var loc = findLocByNumber($(this).attr('data-loc-number'));
+
+				if (loc) pickLoc(loc);
+
+			});
+
+			layero.find('#btnStockLocFilter').off('click').on('click', doSearch);
+
+			layero.find('#stockLocKeyword').off('keypress').on('keypress', function(e){ if (e.which === 13) doSearch(); });
+
+			layero.find('#stockLocKeyword').off('input.stockLocFilter').on('input.stockLocFilter', function() {
+
+				doSearch();
+
+			});
+
+			doSearch();
+
+		}
+
+	});
+
+}
+
+
+
+function loadStockLocOptions() {
+
+	var stockNumber = $.trim($('#stockNumber').val() || '');
+
+	var $wrap = $('#stockLocWrap');
+
+	var $hint = $('#stockLocHint');
+
+	var $requiredMark = $('#stockLocRequiredMark');
+
+	var $searchBtn = $('#btnStockLocSearch');
+
+	if (stockNumber === '') {
+
+		$wrap.hide();
+
+		stockLocRequired = false;
+
+		stockLocOptions = [];
+
+		clearStockLocSelection();
+
+		$hint.text('');
+
+		$requiredMark.hide();
+
+		$searchBtn.hide();
+
+		return;
+
+	}
+
+	$wrap.show();
+
+	clearStockLocSelection();
+
+	$('#stockLocDisplay').attr('placeholder', '${text("加载中...")}');
+
+	$hint.text('');
+
+	$searchBtn.hide();
+
+	$.get(stockLocOptionsUrl, { stockNumber: stockNumber }, function(data){
+
+		stockLocOptions = (data && data.locations) ? data.locations : [];
+
+		stockLocRequired = !!(data && data.required);
+
+		if (stockLocRequired) {
+
+			$requiredMark.show();
+
+			$searchBtn.show();
+
+		} else {
+
+			$requiredMark.hide();
+
+			$searchBtn.hide();
+
+		}
+
+		var defaultLoc = (data && data.defaultLocNumber) ? data.defaultLocNumber : '';
+
+		if (stockLocOptions.length === 0) {
+
+			$('#stockLocDisplay').val('').attr('placeholder', '--');
+
+		} else {
+
+			$('#stockLocDisplay').attr('placeholder', '${text("请点击右侧按钮选择仓位")}');
+
+			if (defaultLoc !== '') {
+
+				for (var i = 0; i < stockLocOptions.length; i++) {
+
+					if (stockLocOptions[i].locNumber === defaultLoc) {
+
+						applyStockLocSelection(stockLocOptions[i]);
+
+						break;
+
+					}
+
+				}
+
+			}
+
+		}
+
+		$hint.text((data && data.locHint) ? data.locHint : '');
+
+	});
+
+}
+
+
+
+$(function(){
+
+	loadStockLocOptions();
+
+	$('#stockNumber').on('change', loadStockLocOptions);
+
+	$('#btnStockLocSearch, #stockLocDisplay').on('click', function(e) {
+
+		e.preventDefault();
+
+		openStockLocSearchPopup();
+
+	});
+
+});
+
+
+
+function escapeHtml(text) {
+
+	if (text == null) return '';
+
+	return String(text).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+
+}
+
+
+
+function readRowLotNumber($row) {
+
+	var hidden = $.trim($row.find('.instock-lot-value').val() || '');
+
+	if (hidden !== '') return hidden;
+
+	return $.trim($row.find('.instock-lot-input').val() || '');
+
+}
+
+
+
+function applyLotSelection(entryId, lotNumber, hint) {
+
+	var $row = $('tr.inspect-row[data-entry-id="'+entryId+'"]');
+
+	$row.find('.instock-lot-value').val(lotNumber);
+
+	$row.find('.instock-lot-input').val(lotNumber);
+
+	if (hint) {
+
+		var $hint = $row.find('.instock-lot-hint');
+
+		if ($hint.length === 0) {
+
+			$row.find('.instock-lot-wrap').append('<small class="text-success instock-lot-hint"></small>');
+
+			$hint = $row.find('.instock-lot-hint');
+
+		}
+
+		$hint.removeClass('text-muted text-warning text-danger').addClass('text-success').text(hint);
+
+	}
+
+}
+
+
+
+function renderLotSearchTable(lots) {
+
+	if (!lots || lots.length === 0) {
+
+		return '<div class="text-muted" style="padding:10px;">${text("未查到批号档案")}</div>';
+
+	}
+
+	var html = '<div class="lot-search-table-wrap"><table class="table table-bordered table-hover table-condensed"><thead><tr>'+
+
+		'<th style="width:220px;">${text("批号")}</th><th style="width:80px;">${text("操作")}</th></tr></thead><tbody>';
+
+	for (var i = 0; i < lots.length; i++) {
+
+		var lotNo = lots[i].lotNumber || '';
+
+		html += '<tr><td>'+escapeHtml(lotNo)+'</td><td align="center">'+
+
+			'<button type="button" class="btn btn-xs btn-primary lot-search-pick" data-idx="'+i+'">${text("选择")}</button></td></tr>';
+
+	}
+
+	html += '</tbody></table></div>';
+
+	return html;
+
+}
+
+
+
+function openLotSearchPopup($btn) {
+
+	var entryId = String($btn.data('entry-id'));
+
+	var materialNumber = $btn.data('material') || '';
+
+	js.layer.open({
+
+		type: 1,
+
+		area: ['640px', '480px'],
+
+		title: '${text("ERP 批号档案")} - ' + ($btn.data('material-name') || materialNumber),
+
+		resize: false,
+
+		scrollbar: true,
+
+		content: js.template('lotSearchTpl', {
+
+			materialNumber: materialNumber,
+
+			materialName: $btn.data('material-name') || ''
+
+		}),
+
+		success: function(layero, index) {
+
+			layero = $(layero);
+
+			var currentLots = [];
+
+			var pickLot = function(lot) {
+
+				if (!lot || !lot.lotNumber) return;
+
+				applyLotSelection(entryId, lot.lotNumber, '${text("已选批号")} ' + lot.lotNumber);
+
+				js.layer.close(index);
+
+			};
+
+			var doSearch = function() {
+
+				layero.find('#lotSearchResult').html('<div class="text-muted" style="padding:10px;">${text("查询中...")}</div>');
+
+				$.getJSON(lotSearchUrl, {
+
+					materialNumber: materialNumber,
+
+					lotKeyword: $.trim(layero.find('#lotKeyword').val() || '')
+
+				}, function(data) {
+
+					currentLots = data.lots || [];
+
+					layero.find('#lotSearchHint').text(data.lotHint || '');
+
+					layero.find('#lotSearchResult').html(renderLotSearchTable(currentLots));
+
+				});
+
+			};
+
+			layero.off('click.lotPick').on('click.lotPick', '.lot-search-pick', function(e) {
+
+				e.preventDefault();
+
+				var idx = parseInt($(this).attr('data-idx'), 10);
+
+				if (isNaN(idx)) return;
+
+				pickLot(currentLots[idx]);
+
+			});
+
+			layero.find('#btnLotSearch').off('click').on('click', doSearch);
+
+			layero.find('#lotKeyword').off('keypress').on('keypress', function(e){ if (e.which === 13) doSearch(); });
+
+			doSearch();
+
+		}
+
+	});
+
+}
+
+
+
+$(document).on('click', '.instock-lot-open', function(e) {
+
+	e.preventDefault();
+
+	openLotSearchPopup($(this));
+
+});
+
+
+
+$(document).on('input', '.instock-lot-input', function() {
+
+	var $row = $(this).closest('tr.inspect-row');
+
+	$row.find('.instock-lot-value').val($.trim($(this).val()));
+
+});
+
+
+
+$('#btnSubmitInStock').on('click', function(){
+
+	var lines = [];
+
+	var hasError = false;
+
+	$('#inspectTable tbody tr.inspect-row').each(function(){
+
+		var $row = $(this);
+
+		var entryId = $row.data('entry-id');
+
+		var seq = $row.data('seq');
+
+		var remainQty = parseFloat($row.data('remain')) || 0;
+
+		var batchManage = String($row.data('batch-manage')) === '1';
+
+		var inspectQty = parseFloat($row.find('.inspect-qty').val());
+
+		if (isNaN(inspectQty) || inspectQty <= 0) {
+
+			js.showMessage('${text("行")} ' + seq + ' ${text("请输入有效的检验数量")}');
+
+			hasError = true;
+
+			return false;
+
+		}
+
+		if (inspectQty > remainQty) {
+
+			js.showMessage('${text("行")} ' + seq + ' ${text("检验数量不能超过剩余可入数量")} ' + remainQty);
+
+			hasError = true;
+
+			return false;
+
+		}
+
+		var line = { entryId: String(entryId), inspectQty: inspectQty };
+
+		if (batchManage) {
+
+			var lotNumber = readRowLotNumber($row);
+
+			if (lotNumber === '') {
+
+				js.showMessage('${text("行")} ' + seq + ' ${text("启用批号管理,请从 ERP 选择或手工录入批号")}');
+
+				hasError = true;
+
+				return false;
+
+			}
+
+			line.lotNumber = lotNumber;
+
+		}
+
+		lines.push(line);
+
+	});
+
+	if (hasError) return;
+
+	if (lines.length === 0) {
+
+		js.showMessage('${text("没有可提交的入库明细")}');
+
+		return;
+
+	}
+
+	var stockNumber = $.trim($('#stockNumber').val() || '');
+
+	if (stockNumber === '') {
+
+		js.showMessage('${text("请选择入库仓库")}');
+
+		return;
+
+	}
+
+	var stockLocNumber = '';
+
+	if (stockLocRequired) {
+
+		stockLocNumber = $.trim($('#stockLocNumber').val() || '');
+
+		if (stockLocNumber === '') {
+
+			js.showMessage('${text("请选择入库仓位")}');
+
+			return;
+
+		}
+
+	}
+
+	js.confirm('${text("确认提交")} ' + lines.length + ' ${text("行到 ERP 生成一张采购入库单?")}', function(){
+
+		$.ajax({
+
+			url: '${ctx}/mes/erpInStock/submitInStock',
+
+			type: 'POST',
+
+			contentType: 'application/json;charset=UTF-8',
+
+			data: JSON.stringify({ billNo: '${billNo!}', stockNumber: stockNumber, stockLocNumber: stockLocNumber, lines: lines }),
+
+			success: function(data){
+
+				js.showMessage(data.message);
+
+				if (data.result == Global.TRUE) {
+
+					window.location.href = '${ctx}/mes/erpInStock/list';
+
+				}
+
+			}
+
+		});
+
+	});
+
+});
+
+</script>
+

+ 88 - 0
src/main/resources/views/modules/mes/erpInStockList.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-truck"></i> ${text('ERP采购入库单查询')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpInStock/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">
+			<#form:form id="searchForm" model="${erpInStock}" action="${ctx}/mes/erpInStock/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="poOrderNoQuery" 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="supplierNumberQuery" maxlength="100" class="form-control width-160"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('物料编码')}:</label>
+					<div class="control-inline">
+						<#form:input path="materialNumberQuery" maxlength="100" class="form-control width-180"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<label class="control-label">${text('单据状态')}:</label>
+					<div class="control-inline width-120">
+						<select name="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/erpInStock/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP采购入库单明细")}">'+val+'</a>';
+		}},
+		{header:'${text("入库日期")}', name:'date', index:'date', width:110, align:"center", frozen:true},
+		{header:'${text("采购订单")}', name:'poOrderNo', index:'poOrderNo', width:170, align:"center", formatter: function(val){
+			if (!val) { return ''; }
+			return '<a href="${ctx}/mes/erpPurchaseOrder/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP采购订单明细")}">'+val+'</a>';
+		}},
+		{header:'${text("供应商")}', name:'supplierName', index:'supplierName', width:160, align:"left"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:180, align:"left"},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:200, align:"left"},
+		{header:'${text("实收数量")}', name:'realQty', index:'realQty', width:100, align:"right", formatter: formatErpQty},
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:70, align:"center"},
+		{header:'${text("批号")}', name:'lotNumber', index:'lotNumber', width:130, align:"center"},
+		{header:'${text("仓库")}', name:'stockName', index:'stockName', width:120, align:"left"},
+		{header:'${text("收料组织")}', name:'stockOrgName', index:'stockOrgName', width:160, align:"left"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

+ 136 - 0
src/main/resources/views/modules/mes/erpInStockSelectDetail.html

@@ -0,0 +1,136 @@
+<% 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('增加采购入库 - 选择订单明细')}:${erpPurchaseOrder.billNo}
+
+			</div>
+
+			<div class="box-tools pull-right">
+
+				<a href="javascript:void(0)" class="btn btn-primary" id="btnBatchInspect"><i class="fa fa-check-square-o"></i> ${text('来料检')}</a>
+
+				<a href="${ctx}/mes/erpInStock/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;padding:8px 12px;">
+
+				<i class="fa fa-info-circle"></i> ${text('勾选需要入库的分录后点「来料检」,多行将合并生成一张采购入库单(与 ERP 一致)。')}
+
+			</div>
+
+			<#form:form id="searchForm" model="${erpPurchaseOrder}" action="${ctx}/mes/erpInStock/selectDetailData" 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>
+
+var poBillNo = '${erpPurchaseOrder.billNo!}';
+
+
+
+function getSelectedEntryIds() {
+
+	var entryIds = [];
+
+	$('.instock-select:checked').each(function(){
+
+		entryIds.push($(this).data('entry-id'));
+
+	});
+
+	return entryIds;
+
+}
+
+
+
+$('#btnBatchInspect').on('click', function(){
+
+	var entryIds = getSelectedEntryIds();
+
+	if (entryIds.length === 0) {
+
+		js.showMessage('${text("请至少勾选一行分录")}');
+
+		return;
+
+	}
+
+	var url = '${ctx}/mes/erpInStock/inspect?billNo=' + encodeURIComponent(poBillNo)
+
+		+ '&entryIds=' + encodeURIComponent(entryIds.join(','));
+
+	js.addTabPage(null, '${text("来料检与入库")}', url, true);
+
+});
+
+
+
+$('#dataGrid').dataGrid({
+
+	searchForm: $("#searchForm"),
+
+	columnModel: [
+
+		{header:'${text("选择")}', name:'lineSelect', width:50, align:"center", frozen:true, formatter: function(val, obj, row){
+
+			return '<input type="checkbox" class="instock-select" data-entry-id="'+row.entryId+'" checked="checked"/>';
+
+		}},
+
+		{header:'${text("行号")}', name:'seq', index:'seq', width:70, align:"center", frozen:true},
+
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:180, align:"left"},
+
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:220, align:"left"},
+
+		{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: formatErpQty},
+
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:70, align:"center"},
+
+		{header:'${text("供应商")}', name:'supplierName', index:'supplierName', width:160, align:"left"},
+
+		{header:'${text("交货日期")}', name:'deliveryDate', index:'deliveryDate', width:110, align:"center"}
+
+	],
+
+	ajaxSuccess: function(){}
+
+});
+
+</script>
+

+ 157 - 0
src/main/resources/views/modules/mes/erpInStockSelectOrder.html

@@ -0,0 +1,157 @@
+<% 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-shopping-cart"></i> ${text('增加采购入库 - 选择采购订单')}
+
+			</div>
+
+			<div class="box-tools pull-right">
+
+				<a href="${ctx}/mes/erpInStock/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">
+
+			<div class="alert alert-info" style="margin-bottom:10px;padding:8px 12px;">
+
+				<i class="fa fa-info-circle"></i> ${text('可按订单号、物料编码、单据状态筛选;进入下一步后再筛选剩余可入数量 > 0 的分录。')}
+
+			</div>
+
+			<#form:form id="searchForm" model="${erpPurchaseOrder}" action="${ctx}/mes/erpInStock/selectOrderData" method="post" class="form-inline"
+
+					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">
+
+					<label class="control-label">${text('物料编码')}:</label>
+
+					<div class="control-inline">
+
+						<#form:input path="materialNumberQuery" maxlength="100" class="form-control width-180"/>
+
+					</div>
+
+				</div>
+
+				<div class="form-group">
+
+					<label class="control-label">${text('单据状态')}:</label>
+
+					<div class="control-inline width-120">
+
+						<select name="documentStatusQuery" id="documentStatusQuery" class="form-control">
+
+							<option value="" <% if(isBlank(docStatus!'')){ %>selected<% } %>>${text('全部')}</option>
+
+							<option value="A" <% if(docStatus!'C' == 'A'){ %>selected<% } %>>${text('创建')}</option>
+
+							<option value="B" <% if(docStatus!'C' == 'B'){ %>selected<% } %>>${text('审核中')}</option>
+
+							<option value="C" <% if(docStatus!'C' == 'C'){ %>selected<% } %>>${text('已审核')}</option>
+
+							<option value="D" <% if(docStatus!'C' == 'D'){ %>selected<% } %>>${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:'actions', width:100, align:"center", frozen:true, formatter: function(val, obj, row){
+
+			return '<a href="${ctx}/mes/erpInStock/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, formatter: function(val){
+
+			return '<a href="${ctx}/mes/erpPurchaseOrder/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP采购订单明细")}">'+val+'</a>';
+
+		}},
+
+		{header:'${text("采购日期")}', name:'date', index:'date', width:110, align:"center"},
+
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:90, align:"center"},
+
+		{header:'${text("供应商")}', name:'supplierName', index:'supplierName', width:180, align:"left"},
+
+		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:90, align:"right"},
+
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:110, align:"right", formatter: formatErpQty}
+
+	],
+
+	ajaxSuccess: function(){}
+
+});
+
+$('#searchForm').on('reset', function(){
+
+	setTimeout(function(){
+
+		$('#documentStatusQuery').val('C');
+
+	}, 0);
+
+});
+
+</script>
+
+

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

@@ -30,8 +30,8 @@ $('#dataGrid').dataGrid({
 		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:150, align:"left"},
 		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:160, align:"left"},
 		{header:'${text("单位")}', name:'unitName', index:'unitName', width:70, align:"center"},
-		{header:'${text("申请数量")}', name:'appQty', index:'appQty', width:90, align:"right"},
-		{header:'${text("实发数量")}', name:'actualQty', index:'actualQty', width:90, align:"right"},
+		{header:'${text("申请数量")}', name:'appQty', index:'appQty', width:90, align:"right", formatter: formatErpQty},
+		{header:'${text("实发数量")}', name:'actualQty', index:'actualQty', width:90, align:"right", formatter: formatErpQty},
 		{header:'${text("用料清单")}', name:'ppbomBillNo', index:'ppbomBillNo', width:140, align:"center"},
 		{header:'${text("车间")}', name:'workshopName', index:'workshopName', width:120, align:"left"},
 		{header:'${text("仓库")}', name:'stockName', index:'stockName', width:120, align:"left"}

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

@@ -116,9 +116,9 @@ $('#dataGrid').dataGrid({
 
 		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:90, align:"right"},
 
-		{header:'${text("申请总数量")}', name:'totalAppQty', index:'totalAppQty', width:100, align:"right"},
+		{header:'${text("申请总数量")}', name:'totalAppQty', index:'totalAppQty', width:100, align:"right", formatter: formatErpQty},
 
-		{header:'${text("实发总数量")}', name:'totalActualQty', index:'totalActualQty', width:100, align:"right"}
+		{header:'${text("实发总数量")}', name:'totalActualQty', index:'totalActualQty', width:100, align:"right", formatter: formatErpQty}
 
 	],
 

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

@@ -533,11 +533,11 @@ $('#dataGrid').dataGrid({
 
 		{header:'${text("子项物料名称")}', name:'materialName', index:'materialName', width:260, align:"left"},
 
-		{header:'${text("应发数量")}', name:'mustQty', index:'mustQty', width:100, align:"right"},
+		{header:'${text("应发数量")}', name:'mustQty', index:'mustQty', width:100, align:"right", formatter: formatErpQty},
 
-		{header:'${text("已领数量")}', name:'pickedQty', index:'pickedQty', width:100, align:"right"},
+		{header:'${text("已领数量")}', name:'pickedQty', index:'pickedQty', width:100, align:"right", formatter: formatErpQty},
 
-		{header:'${text("未领数量")}', name:'noPickedQty', index:'noPickedQty', width:100, align:"right"},
+		{header:'${text("未领数量")}', name:'noPickedQty', index:'noPickedQty', width:100, align:"right", formatter: formatErpQty},
 
 		{header:'${text("可领数量")}', name:'canPickQty', index:'canPickQty', width:90, align:"right", formatter: formatCanPickQty},
 

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

@@ -34,7 +34,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:260, 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:'qty', index:'qty', width:110, align:"right"},
+		{header:'${text("数量")}', name:'qty', index:'qty', width:110, align:"right", formatter: formatErpQty},
 		{header:'${text("单位")}', name:'unitName', index:'unitName', width:90, align:"center"},
 		{header:'${text("业务状态")}', name:'businessStatusName', index:'businessStatusName', width:100, align:"center"},
 		{header:'${text("计划开工")}', name:'planStartDate', index:'planStartDate', width:150, align:"center"},

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

@@ -46,7 +46,7 @@ $('#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"}
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right", formatter: formatErpQty}
 	],
 	ajaxSuccess: function(){}
 });

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

@@ -34,7 +34,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:260, 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:'qty', index:'qty', width:110, align:"right"},
+		{header:'${text("数量")}', name:'qty', index:'qty', width:110, align:"right", formatter: formatErpQty},
 		{header:'${text("单位")}', name:'unitName', index:'unitName', width:90, align:"center"},
 		{header:'${text("业务状态")}', name:'businessStatusName', index:'businessStatusName', width:100, align:"center"},
 		{header:'${text("计划开工")}', name:'planStartDate', index:'planStartDate', width:150, align:"center"},

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

@@ -43,7 +43,7 @@ $('#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"}
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right", formatter: formatErpQty}
 	],
 	ajaxSuccess: function(){}
 });

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

@@ -28,7 +28,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("行号")}', name:'seq', index:'seq', width:70, align:"center", frozen:true},
 		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:180, align:"left", frozen:true},
 		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:220, align:"left"},
-		{header:'${text("采购数量")}', name:'qty', index:'qty', width:100, align:"right"},
+		{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:'deliveryDate', index:'deliveryDate', width:110, align:"center"},
 		{header:'${text("单价")}', name:'price', index:'price', width:100, align:"right"},

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

@@ -57,7 +57,7 @@ $('#dataGrid').dataGrid({
 		{header:'${text("供应商编码")}', name:'supplierNumber', index:'supplierNumber', width:130, align:"left"},
 		{header:'${text("供应商名称")}', name:'supplierName', index:'supplierName', width:180, align:"left"},
 		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:90, align:"right"},
-		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:110, align:"right"}
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:110, align:"right", formatter: formatErpQty}
 	],
 	ajaxSuccess: function(){}
 });