Kaynağa Gözat

ERP对接,生产订单抓取

jingbo 4 ay önce
ebeveyn
işleme
bc42f94be5

+ 190 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpProductionMaterial.java

@@ -0,0 +1,190 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+public class ErpProductionMaterial implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private String id;
+	private String ppbomId;
+	private String ppbomNo;
+	private String billNo;
+	private String moEntryId;
+	private String seq;
+	private String materialNumber;
+	private String materialName;
+	private String specification;
+	private BigDecimal mustQty;
+	private BigDecimal pickedQty;
+	private BigDecimal noPickedQty;
+	private BigDecimal issueQty;
+	private String unitName;
+	private String stockNumber;
+	private String stockName;
+	private String sourceStockNumber;
+	private String sourceStockName;
+	private String stockStatusName;
+	private String lotText;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getPpbomId() {
+		return ppbomId;
+	}
+
+	public void setPpbomId(String ppbomId) {
+		this.ppbomId = ppbomId;
+	}
+
+	public String getPpbomNo() {
+		return ppbomNo;
+	}
+
+	public void setPpbomNo(String ppbomNo) {
+		this.ppbomNo = ppbomNo;
+	}
+
+	public String getBillNo() {
+		return billNo;
+	}
+
+	public void setBillNo(String billNo) {
+		this.billNo = billNo;
+	}
+
+	public String getMoEntryId() {
+		return moEntryId;
+	}
+
+	public void setMoEntryId(String moEntryId) {
+		this.moEntryId = moEntryId;
+	}
+
+	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 getSpecification() {
+		return specification;
+	}
+
+	public void setSpecification(String specification) {
+		this.specification = specification;
+	}
+
+	public BigDecimal getMustQty() {
+		return mustQty;
+	}
+
+	public void setMustQty(BigDecimal mustQty) {
+		this.mustQty = mustQty;
+	}
+
+	public BigDecimal getPickedQty() {
+		return pickedQty;
+	}
+
+	public void setPickedQty(BigDecimal pickedQty) {
+		this.pickedQty = pickedQty;
+	}
+
+	public BigDecimal getNoPickedQty() {
+		return noPickedQty;
+	}
+
+	public void setNoPickedQty(BigDecimal noPickedQty) {
+		this.noPickedQty = noPickedQty;
+	}
+
+	public BigDecimal getIssueQty() {
+		return issueQty;
+	}
+
+	public void setIssueQty(BigDecimal issueQty) {
+		this.issueQty = issueQty;
+	}
+
+	public String getUnitName() {
+		return unitName;
+	}
+
+	public void setUnitName(String unitName) {
+		this.unitName = unitName;
+	}
+
+	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 getSourceStockNumber() {
+		return sourceStockNumber;
+	}
+
+	public void setSourceStockNumber(String sourceStockNumber) {
+		this.sourceStockNumber = sourceStockNumber;
+	}
+
+	public String getSourceStockName() {
+		return sourceStockName;
+	}
+
+	public void setSourceStockName(String sourceStockName) {
+		this.sourceStockName = sourceStockName;
+	}
+
+	public String getStockStatusName() {
+		return stockStatusName;
+	}
+
+	public void setStockStatusName(String stockStatusName) {
+		this.stockStatusName = stockStatusName;
+	}
+
+	public String getLotText() {
+		return lotText;
+	}
+
+	public void setLotText(String lotText) {
+		this.lotText = lotText;
+	}
+}

+ 224 - 0
src/main/java/com/jeesite/modules/mes/erp/ErpProductionMaterialService.java

@@ -0,0 +1,224 @@
+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.List;
+
+@Service
+public class ErpProductionMaterialService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpProductionMaterialService.class);
+	private static final String FORM_ID = "PRD_PPBOM";
+	private static final String QUERY_FIELD_KEYS = "FID,FBILLNO";
+	private static final String[] MO_BILL_NO_FIELDS = {"MoBillNo", "MOBillNO", "FMoBillNo", "FMoBillNO"};
+	private static final String[] MO_ENTRY_ID_FIELDS = {"MoEntryId", "MOEntryID", "FMoEntryId", "FMOEntryID"};
+
+	private final KingdeeErpClient erpClient;
+
+	public ErpProductionMaterialService(KingdeeErpClient erpClient) {
+		this.erpClient = erpClient;
+	}
+
+	public List<ErpProductionMaterial> findByProductionOrderEntry(String billNo, String entryId) {
+		if (StringUtils.isBlank(billNo) || StringUtils.isBlank(entryId)) {
+			return new ArrayList<>();
+		}
+		JSONObject ppbom = findPpbom(billNo, entryId);
+		if (ppbom == null) {
+			return new ArrayList<>();
+		}
+		return findDetailByNumber(ppbom.getString("FBILLNO"), billNo, entryId);
+	}
+
+	private JSONObject findPpbom(String billNo, String entryId) {
+		JSONObject direct = findPpbomByCandidateFields(billNo, entryId);
+		if (direct != null) {
+			return direct;
+		}
+		return findPpbomByRecentView(billNo, entryId);
+	}
+
+	private JSONObject findPpbomByCandidateFields(String billNo, String entryId) {
+		for (String billNoField : MO_BILL_NO_FIELDS) {
+			for (String entryIdField : MO_ENTRY_ID_FIELDS) {
+				try {
+					JSONObject ppbom = doFindPpbom(billNoField, entryIdField, billNo, entryId);
+					if (ppbom != null) {
+						logger.info("ERP production material matched by fields: {}, {}", billNoField, entryIdField);
+						return ppbom;
+					}
+				} catch (RuntimeException e) {
+					logger.debug("ERP production material field unavailable: {}, {}, {}", billNoField, entryIdField, e.getMessage());
+				}
+			}
+		}
+		return null;
+	}
+
+	private JSONObject doFindPpbom(String billNoField, String entryIdField, String billNo, String entryId) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", QUERY_FIELD_KEYS);
+		data.put("FilterString", billNoField + "='" + escape(billNo) + "' AND " + entryIdField + "=" + escapeNumber(entryId));
+		data.put("OrderString", "FBILLNO DESC");
+		data.put("StartRow", 0);
+		data.put("Limit", 1);
+		logger.info("ERP production material ppbom query: {}", data.toJSONString());
+		JSONArray rows = erpClient.billQuery(data);
+		if (rows.isEmpty()) {
+			return null;
+		}
+		return rows.getJSONObject(0);
+	}
+
+	private JSONObject findPpbomByRecentView(String billNo, String entryId) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", QUERY_FIELD_KEYS);
+		data.put("FilterString", "");
+		data.put("OrderString", "FBILLNO DESC");
+		data.put("StartRow", 0);
+		data.put("Limit", 200);
+		logger.info("ERP production material fallback query: {}", data.toJSONString());
+		JSONArray rows = erpClient.billQuery(data);
+		for (int i = 0; i < rows.size(); i++) {
+			JSONObject row = rows.getJSONObject(i);
+			JSONObject result = viewResult(row.getString("FBILLNO"));
+			if (result != null && matchesProductionOrder(result, billNo, entryId)) {
+				return row;
+			}
+		}
+		return null;
+	}
+
+	private List<ErpProductionMaterial> findDetailByNumber(String ppbomNo, String billNo, String entryId) {
+		JSONObject data = new JSONObject();
+		data.put("CreateOrgId", 0);
+		data.put("Number", ppbomNo);
+		data.put("Id", "");
+		data.put("IsSortBySeq", false);
+		JSONObject response = erpClient.view(FORM_ID, data);
+		JSONObject result = viewResult(response);
+		if (result == null) {
+			return new ArrayList<>();
+		}
+		String ppbomId = result.getString("Id");
+		JSONArray entries = result.getJSONArray("PPBomEntry");
+		List<ErpProductionMaterial> list = new ArrayList<>();
+		if (entries == null) {
+			return list;
+		}
+		for (int i = 0; i < entries.size(); i++) {
+			JSONObject entry = entries.getJSONObject(i);
+			if (!entryId.equals(entry.getString("MoEntryId"))) {
+				continue;
+			}
+			list.add(toMaterial(entry, ppbomId, ppbomNo, billNo, entryId));
+		}
+		return list;
+	}
+
+	private JSONObject viewResult(String ppbomNo) {
+		JSONObject data = new JSONObject();
+		data.put("CreateOrgId", 0);
+		data.put("Number", ppbomNo);
+		data.put("Id", "");
+		data.put("IsSortBySeq", false);
+		return viewResult(erpClient.view(FORM_ID, data));
+	}
+
+	private JSONObject viewResult(JSONObject response) {
+		JSONObject resultWrapper = response.getJSONObject("Result");
+		JSONObject status = resultWrapper == null ? null : resultWrapper.getJSONObject("ResponseStatus");
+		if (status == null || !Boolean.TRUE.equals(status.getBoolean("IsSuccess"))) {
+			logger.warn("ERP production material view failed, response={}", response.toJSONString());
+			return null;
+		}
+		return resultWrapper.getJSONObject("Result");
+	}
+
+	private boolean matchesProductionOrder(JSONObject result, String billNo, String entryId) {
+		if (billNo.equals(result.getString("MOBillNO")) && entryId.equals(result.getString("MOEntryID"))) {
+			return true;
+		}
+		JSONArray entries = result.getJSONArray("PPBomEntry");
+		if (entries == null) {
+			return false;
+		}
+		for (int i = 0; i < entries.size(); i++) {
+			JSONObject entry = entries.getJSONObject(i);
+			if (billNo.equals(entry.getString("MoBillNo")) && entryId.equals(entry.getString("MoEntryId"))) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private ErpProductionMaterial toMaterial(JSONObject row, String ppbomId, String ppbomNo, String billNo, String entryId) {
+		ErpProductionMaterial material = new ErpProductionMaterial();
+		material.setId(row.getString("Id"));
+		material.setPpbomId(ppbomId);
+		material.setPpbomNo(ppbomNo);
+		material.setBillNo(billNo);
+		material.setMoEntryId(entryId);
+		material.setSeq(row.getString("Seq"));
+		JSONObject materialObj = row.getJSONObject("MaterialID");
+		material.setMaterialNumber(materialObj == null ? "" : materialObj.getString("Number"));
+		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.setUnitName(getNameValue(row.getJSONObject("UnitID")));
+		JSONObject stock = row.getJSONObject("StockID");
+		material.setStockNumber(stock == null ? "" : stock.getString("Number"));
+		material.setStockName(getNameValue(stock));
+		JSONObject sourceStock = row.getJSONObject("SrcTransStockId");
+		material.setSourceStockNumber(sourceStock == null ? "" : sourceStock.getString("Number"));
+		material.setSourceStockName(getNameValue(sourceStock));
+		material.setStockStatusName(getNameValue(row.getJSONObject("StockStatusId")));
+		material.setLotText(row.getString("Lot_Text"));
+		return material;
+	}
+
+	private String getSpecification(JSONObject object) {
+		if (object == null) {
+			return "";
+		}
+		JSONArray specification = object.getJSONArray("Specification");
+		if (specification == null || specification.isEmpty()) {
+			return "";
+		}
+		return specification.getJSONObject(0).getString("Value");
+	}
+
+	private String getNameValue(JSONObject object) {
+		if (object == null) {
+			return "";
+		}
+		JSONArray nameArray = object.getJSONArray("Name");
+		if (nameArray == null || nameArray.isEmpty()) {
+			return "";
+		}
+		return nameArray.getJSONObject(0).getString("Value");
+	}
+
+	private String escape(String value) {
+		return value == null ? "" : value.trim().replace("'", "''");
+	}
+
+	private String escapeNumber(String value) {
+		String trimmed = value == null ? "" : value.trim();
+		if (!trimmed.matches("\\d+")) {
+			throw new IllegalArgumentException("生产订单分录内码必须是数字");
+		}
+		return trimmed;
+	}
+}

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

@@ -0,0 +1,265 @@
+package com.jeesite.modules.mes.erp;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * ERP production order row returned by Kingdee BillQuery.
+ */
+public class ErpProductionOrder 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 prdOrgName;
+	private String workshopName;
+	private String plannerName;
+	private String inventoryCategory;
+	private Integer entryCount;
+	private BigDecimal totalQty;
+	private String entryId;
+	private String seq;
+	private String materialNumber;
+	private String materialName;
+	private String specification;
+	private BigDecimal qty;
+	private String unitName;
+	private String planStartDate;
+	private String planFinishDate;
+	private String businessStatus;
+	private String businessStatusName;
+	private String stockName;
+	private String bomNumber;
+	private String routingNumber;
+	private String projectNo;
+	private String mtoNo;
+
+	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 getPrdOrgName() {
+		return prdOrgName;
+	}
+
+	public void setPrdOrgName(String prdOrgName) {
+		this.prdOrgName = prdOrgName;
+	}
+
+	public String getWorkshopName() {
+		return workshopName;
+	}
+
+	public void setWorkshopName(String workshopName) {
+		this.workshopName = workshopName;
+	}
+
+	public String getPlannerName() {
+		return plannerName;
+	}
+
+	public void setPlannerName(String plannerName) {
+		this.plannerName = plannerName;
+	}
+
+	public String getInventoryCategory() {
+		return inventoryCategory;
+	}
+
+	public void setInventoryCategory(String inventoryCategory) {
+		this.inventoryCategory = inventoryCategory;
+	}
+
+	public Integer getEntryCount() {
+		return entryCount;
+	}
+
+	public void setEntryCount(Integer entryCount) {
+		this.entryCount = entryCount;
+	}
+
+	public BigDecimal getTotalQty() {
+		return totalQty;
+	}
+
+	public void setTotalQty(BigDecimal totalQty) {
+		this.totalQty = totalQty;
+	}
+
+	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 getSpecification() {
+		return specification;
+	}
+
+	public void setSpecification(String specification) {
+		this.specification = specification;
+	}
+
+	public BigDecimal getQty() {
+		return qty;
+	}
+
+	public void setQty(BigDecimal qty) {
+		this.qty = qty;
+	}
+
+	public String getUnitName() {
+		return unitName;
+	}
+
+	public void setUnitName(String unitName) {
+		this.unitName = unitName;
+	}
+
+	public String getPlanStartDate() {
+		return planStartDate;
+	}
+
+	public void setPlanStartDate(String planStartDate) {
+		this.planStartDate = planStartDate;
+	}
+
+	public String getPlanFinishDate() {
+		return planFinishDate;
+	}
+
+	public void setPlanFinishDate(String planFinishDate) {
+		this.planFinishDate = planFinishDate;
+	}
+
+	public String getBusinessStatus() {
+		return businessStatus;
+	}
+
+	public void setBusinessStatus(String businessStatus) {
+		this.businessStatus = businessStatus;
+	}
+
+	public String getBusinessStatusName() {
+		return businessStatusName;
+	}
+
+	public void setBusinessStatusName(String businessStatusName) {
+		this.businessStatusName = businessStatusName;
+	}
+
+	public String getStockName() {
+		return stockName;
+	}
+
+	public void setStockName(String stockName) {
+		this.stockName = stockName;
+	}
+
+	public String getBomNumber() {
+		return bomNumber;
+	}
+
+	public void setBomNumber(String bomNumber) {
+		this.bomNumber = bomNumber;
+	}
+
+	public String getRoutingNumber() {
+		return routingNumber;
+	}
+
+	public void setRoutingNumber(String routingNumber) {
+		this.routingNumber = routingNumber;
+	}
+
+	public String getProjectNo() {
+		return projectNo;
+	}
+
+	public void setProjectNo(String projectNo) {
+		this.projectNo = projectNo;
+	}
+
+	public String getMtoNo() {
+		return mtoNo;
+	}
+
+	public void setMtoNo(String mtoNo) {
+		this.mtoNo = mtoNo;
+	}
+}

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

@@ -0,0 +1,471 @@
+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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class ErpProductionOrderService {
+
+	private static final Logger logger = LoggerFactory.getLogger(ErpProductionOrderService.class);
+	private static final String FORM_ID = "PRD_MO";
+	private static final Set<String> ALLOWED_INVENTORY_CATEGORIES =
+			new HashSet<>(Arrays.asList("HEV", "\u6b23\u65fa\u8fbe115", "115"));
+	private static final String HEADER_FIELD_KEYS = String.join(",",
+			"FID",
+			"FBILLNO",
+			"FDocumentStatus",
+			"FDate",
+			"FPrdOrgId.FNAME",
+			"FWorkShopID0.FNAME",
+			"FPlannerID.FNAME",
+			"FDescription");
+	private static final String DETAIL_FIELD_KEYS = String.join(",",
+			"FID",
+			"FBILLNO",
+			"FDocumentStatus",
+			"FDate",
+			"FPrdOrgId.FNAME",
+			"FWorkShopID0.FNAME",
+			"FPlannerID.FNAME",
+			"FTreeEntity_FEntryId",
+			"FTreeEntity_FSeq",
+			"FMATERIALID.FNUMBER",
+			"FMATERIALID.FNAME",
+			"FSpecification",
+			"FQTY",
+			"FUnitId.FNAME",
+			"FPlanStartDate",
+			"FPlanFinishDate",
+			"FStatus",
+			"FStockId.FNAME",
+			"FBomId.FNUMBER",
+			"FRoutingId.FNUMBER",
+			"FProjectNo",
+			"FMTONO");
+
+	private final KingdeeErpClient erpClient;
+	private final KingdeeErpProperties erpProperties;
+
+	public ErpProductionOrderService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties) {
+		this.erpClient = erpClient;
+		this.erpProperties = erpProperties;
+	}
+
+	public List<ErpProductionOrder> findPageList(String billNo, int startRow, int limit) {
+		List<ErpProductionOrder> headers = findFilteredHeaderPageList(billNo, startRow, limit);
+		if (headers.isEmpty()) {
+			return headers;
+		}
+		Map<String, ErpProductionOrder> orderMap = new LinkedHashMap<>();
+		List<String> billNos = new ArrayList<>();
+		for (ErpProductionOrder header : headers) {
+			header.setEntryCount(0);
+			header.setTotalQty(BigDecimal.ZERO);
+			orderMap.put(header.getBillNo(), header);
+			billNos.add(header.getBillNo());
+		}
+		for (ErpProductionOrder detail : findDetailListByBillNos(billNos)) {
+			ErpProductionOrder order = orderMap.get(detail.getBillNo());
+			if (order == null) {
+				continue;
+			}
+			if (StringUtils.isBlank(order.getInventoryCategory())) {
+				order.setInventoryCategory(detail.getInventoryCategory());
+			}
+			order.setEntryCount(order.getEntryCount() + 1);
+			if (detail.getQty() != null) {
+				order.setTotalQty(order.getTotalQty().add(detail.getQty()));
+			}
+		}
+		return new ArrayList<>(orderMap.values());
+	}
+
+	private List<ErpProductionOrder> findFilteredHeaderPageList(String billNo, int startRow, int limit) {
+		if (!erpProperties.isInventoryCategoryFilterEnabled()) {
+			return findHeaderPageList(billNo, startRow, limit);
+		}
+		int targetSize = startRow + limit;
+		int fetchStart = 0;
+		int fetchLimit = Math.max(limit * 5, 50);
+		int maxFetchRows = Math.max(targetSize * 30, 500);
+		List<ErpProductionOrder> matchedHeaders = new ArrayList<>();
+
+		while (matchedHeaders.size() < targetSize && fetchStart < maxFetchRows) {
+			List<ErpProductionOrder> candidates = findHeaderPageList(billNo, fetchStart, fetchLimit);
+			if (candidates.isEmpty()) {
+				break;
+			}
+			for (ErpProductionOrder header : candidates) {
+				String category = findAllowedInventoryCategoryByView(header.getBillNo());
+				if (StringUtils.isBlank(category)) {
+					continue;
+				}
+				header.setInventoryCategory(category);
+				matchedHeaders.add(header);
+				if (matchedHeaders.size() >= targetSize) {
+					break;
+				}
+			}
+			if (candidates.size() < fetchLimit) {
+				break;
+			}
+			fetchStart += fetchLimit;
+		}
+
+		if (startRow >= matchedHeaders.size()) {
+			return new ArrayList<>();
+		}
+		return new ArrayList<>(matchedHeaders.subList(startRow, Math.min(startRow + limit, matchedHeaders.size())));
+	}
+
+	private List<ErpProductionOrder> findHeaderPageList(String billNo, int startRow, int limit) {
+		int targetSize = startRow + limit;
+		int fetchStart = 0;
+		int fetchLimit = Math.max(limit * 5, 100);
+		int maxFetchRows = Math.max(targetSize * 10, 500);
+		Map<String, ErpProductionOrder> uniqueHeaders = new LinkedHashMap<>();
+
+		while (uniqueHeaders.size() < targetSize && fetchStart < maxFetchRows) {
+			List<ErpProductionOrder> chunk = findDetailCandidateList(billNo, fetchStart, fetchLimit);
+			if (chunk.isEmpty()) {
+				break;
+			}
+			for (ErpProductionOrder detail : chunk) {
+				uniqueHeaders.putIfAbsent(detail.getBillNo(), toHeaderFromDetail(detail));
+			}
+			if (chunk.size() < fetchLimit) {
+				break;
+			}
+			fetchStart += fetchLimit;
+		}
+
+		List<ErpProductionOrder> allHeaders = new ArrayList<>(uniqueHeaders.values());
+		if (startRow >= allHeaders.size()) {
+			return new ArrayList<>();
+		}
+		return new ArrayList<>(allHeaders.subList(startRow, Math.min(startRow + limit, allHeaders.size())));
+	}
+
+	private List<ErpProductionOrder> findDetailCandidateList(String billNo, int startRow, int limit) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", buildDetailFieldKeys());
+		data.put("FilterString", buildFilterString(billNo));
+		data.put("OrderString", "FDate DESC,FBILLNO DESC,FTreeEntity_FSeq");
+		data.put("StartRow", Math.max(startRow, 0));
+		data.put("Limit", Math.max(limit, 1));
+		logger.info("ERP production order candidate query: {}", data.toJSONString());
+
+		JSONArray rows = erpClient.billQuery(data);
+		List<ErpProductionOrder> list = new ArrayList<>();
+		for (int i = 0; i < rows.size(); i++) {
+			list.add(toDetail(rows.getJSONObject(i), i));
+		}
+		return list;
+	}
+
+	public List<ErpProductionOrder> findDetailList(String billNo) {
+		if (StringUtils.isBlank(billNo)) {
+			return new ArrayList<>();
+		}
+		return findDetailListByBillNos(singletonList(billNo));
+	}
+
+	private List<ErpProductionOrder> findHeaderList(String billNo, int startRow, int limit) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", HEADER_FIELD_KEYS);
+		data.put("FilterString", buildFilterString(billNo));
+		data.put("OrderString", "FDate DESC,FBILLNO DESC");
+		data.put("StartRow", Math.max(startRow, 0));
+		data.put("Limit", Math.max(limit, 1));
+		logger.info("ERP production order header query: {}", data.toJSONString());
+
+		JSONArray rows = erpClient.billQuery(data);
+		List<ErpProductionOrder> list = new ArrayList<>();
+		for (int i = 0; i < rows.size(); i++) {
+			list.add(toHeader(rows.getJSONObject(i)));
+		}
+		return list;
+	}
+
+	private List<ErpProductionOrder> findDetailListByBillNos(List<String> billNos) {
+		JSONObject data = new JSONObject();
+		data.put("FormId", FORM_ID);
+		data.put("FieldKeys", buildDetailFieldKeys());
+		data.put("FilterString", buildInFilterString(billNos));
+		data.put("OrderString", "FBILLNO DESC,FTreeEntity_FSeq");
+		data.put("Limit", 2000);
+		logger.info("ERP production order detail query: {}", data.toJSONString());
+
+		JSONArray rows = erpClient.billQuery(data);
+		List<ErpProductionOrder> list = new ArrayList<>();
+		Map<String, Map<String, String>> categoryCache = new LinkedHashMap<>();
+		for (int i = 0; i < rows.size(); i++) {
+			ErpProductionOrder detail = toDetail(rows.getJSONObject(i), i);
+			Map<String, String> categories = categoryCache.computeIfAbsent(detail.getBillNo(),
+					this::findAllowedInventoryCategoriesByView);
+			detail.setInventoryCategory(categories.get(detail.getEntryId()));
+			if (!erpProperties.isInventoryCategoryFilterEnabled()
+					|| StringUtils.isNotBlank(detail.getInventoryCategory())) {
+				list.add(detail);
+			}
+		}
+		return list;
+	}
+
+	private ErpProductionOrder toHeader(JSONObject row) {
+		ErpProductionOrder order = new ErpProductionOrder();
+		order.setId(row.getString("FBILLNO"));
+		order.setFid(row.getString("FID"));
+		order.setBillNo(row.getString("FBILLNO"));
+		order.setDocumentStatus(row.getString("FDocumentStatus"));
+		order.setDocumentStatusName(documentStatusName(order.getDocumentStatus()));
+		order.setDate(row.getString("FDate"));
+		order.setPrdOrgName(row.getString("FPrdOrgId.FNAME"));
+		order.setWorkshopName(row.getString("FWorkShopID0.FNAME"));
+		order.setPlannerName(row.getString("FPlannerID.FNAME"));
+		return order;
+	}
+
+	private ErpProductionOrder toHeaderFromDetail(ErpProductionOrder detail) {
+		ErpProductionOrder order = new ErpProductionOrder();
+		order.setId(detail.getBillNo());
+		order.setFid(detail.getFid());
+		order.setBillNo(detail.getBillNo());
+		order.setDocumentStatus(detail.getDocumentStatus());
+		order.setDocumentStatusName(detail.getDocumentStatusName());
+		order.setDate(detail.getDate());
+		order.setPrdOrgName(detail.getPrdOrgName());
+		order.setWorkshopName(detail.getWorkshopName());
+		order.setPlannerName(detail.getPlannerName());
+		order.setInventoryCategory(detail.getInventoryCategory());
+		return order;
+	}
+
+	private ErpProductionOrder toDetail(JSONObject row, int index) {
+		ErpProductionOrder order = new ErpProductionOrder();
+		order.setFid(row.getString("FID"));
+		order.setBillNo(row.getString("FBILLNO"));
+		order.setEntryId(row.getString("FTreeEntity_FEntryId"));
+		order.setSeq(row.getString("FTreeEntity_FSeq"));
+		order.setId(order.getBillNo() + "-" + (StringUtils.isBlank(order.getEntryId()) ? index : order.getEntryId()));
+		order.setDocumentStatus(row.getString("FDocumentStatus"));
+		order.setDocumentStatusName(documentStatusName(order.getDocumentStatus()));
+		order.setDate(row.getString("FDate"));
+		order.setPrdOrgName(row.getString("FPrdOrgId.FNAME"));
+		order.setWorkshopName(row.getString("FWorkShopID0.FNAME"));
+		order.setPlannerName(row.getString("FPlannerID.FNAME"));
+		order.setMaterialNumber(row.getString("FMATERIALID.FNUMBER"));
+		order.setMaterialName(row.getString("FMATERIALID.FNAME"));
+		order.setSpecification(row.getString("FSpecification"));
+		order.setQty(row.getBigDecimal("FQTY"));
+		order.setUnitName(row.getString("FUnitId.FNAME"));
+		order.setPlanStartDate(row.getString("FPlanStartDate"));
+		order.setPlanFinishDate(row.getString("FPlanFinishDate"));
+		order.setBusinessStatus(row.getString("FStatus"));
+		order.setBusinessStatusName(businessStatusName(order.getBusinessStatus()));
+		order.setStockName(row.getString("FStockId.FNAME"));
+		order.setBomNumber(row.getString("FBomId.FNUMBER"));
+		order.setRoutingNumber(row.getString("FRoutingId.FNUMBER"));
+		order.setProjectNo(row.getString("FProjectNo"));
+		order.setMtoNo(row.getString("FMTONO"));
+		return order;
+	}
+
+	private String findAllowedInventoryCategoryByView(String billNo) {
+		Map<String, String> categories = findAllowedInventoryCategoriesByView(billNo);
+		if (categories.isEmpty()) {
+			return "";
+		}
+		return categories.values().iterator().next();
+	}
+
+	private Map<String, String> findAllowedInventoryCategoriesByView(String billNo) {
+		if (StringUtils.isBlank(billNo)) {
+			return Collections.emptyMap();
+		}
+		JSONObject data = new JSONObject();
+		data.put("CreateOrgId", 0);
+		data.put("Number", billNo);
+		data.put("Id", "");
+		data.put("IsSortBySeq", false);
+		JSONObject response = erpClient.view(FORM_ID, data);
+		JSONObject resultWrapper = response.getJSONObject("Result");
+		JSONObject status = resultWrapper == null ? null : resultWrapper.getJSONObject("ResponseStatus");
+		if (status == null || !Boolean.TRUE.equals(status.getBoolean("IsSuccess"))) {
+			logger.warn("ERP production order view failed, billNo={}, response={}", billNo, response.toJSONString());
+			return Collections.emptyMap();
+		}
+		JSONObject result = resultWrapper.getJSONObject("Result");
+		if (result == null) {
+			return Collections.emptyMap();
+		}
+		JSONArray treeEntity = result.getJSONArray("TreeEntity");
+		if (treeEntity == null) {
+			return Collections.emptyMap();
+		}
+		Map<String, String> categories = new LinkedHashMap<>();
+		for (int i = 0; i < treeEntity.size(); i++) {
+			JSONObject entry = treeEntity.getJSONObject(i);
+			String category = extractAllowedCategory(entry);
+			if (StringUtils.isNotBlank(category)) {
+				categories.put(entry.getString("Id"), category);
+			}
+		}
+		return categories;
+	}
+
+	private String extractAllowedCategory(JSONObject entry) {
+		if (entry == null) {
+			return "";
+		}
+		JSONObject material = entry.getJSONObject("MaterialId");
+		JSONArray materialBase = material == null ? null : material.getJSONArray("MaterialBase");
+		if (materialBase == null) {
+			return "";
+		}
+		for (int i = 0; i < materialBase.size(); i++) {
+			JSONObject category = materialBase.getJSONObject(i).getJSONObject("CategoryID");
+			String number = category == null ? "" : category.getString("Number");
+			String name = getNameValue(category);
+			if (isAllowedCategory(number)) {
+				return number;
+			}
+			if (isAllowedCategory(name)) {
+				return name;
+			}
+		}
+		return "";
+	}
+
+	private String getNameValue(JSONObject object) {
+		if (object == null) {
+			return "";
+		}
+		JSONArray nameArray = object.getJSONArray("Name");
+		if (nameArray == null || nameArray.isEmpty()) {
+			return "";
+		}
+		return nameArray.getJSONObject(0).getString("Value");
+	}
+
+	private boolean isAllowedCategory(String value) {
+		if (StringUtils.isBlank(value)) {
+			return false;
+		}
+		return ALLOWED_INVENTORY_CATEGORIES.contains(value.trim());
+	}
+
+	private String buildFilterString(String billNo) {
+		String orgFilter = buildCurrentOrgFilter();
+		String baseFilter = orgFilter;
+		if (StringUtils.isBlank(billNo)) {
+			return baseFilter;
+		}
+		return appendAnd(baseFilter, "FBILLNO='" + billNo.trim().replace("'", "''") + "'");
+	}
+
+	private String buildInFilterString(List<String> billNos) {
+		String baseFilter = buildCurrentOrgFilter();
+		StringBuilder builder = new StringBuilder(baseFilter);
+		if (builder.length() > 0) {
+			builder.append(" AND ");
+		}
+		builder.append("FBILLNO IN(");
+		for (int i = 0; i < billNos.size(); i++) {
+			if (i > 0) {
+				builder.append(",");
+			}
+			builder.append("'").append(billNos.get(i).trim().replace("'", "''")).append("'");
+		}
+		builder.append(")");
+		return builder.toString();
+	}
+
+	private String appendAnd(String left, String right) {
+		if (StringUtils.isBlank(left)) {
+			return right;
+		}
+		if (StringUtils.isBlank(right)) {
+			return left;
+		}
+		return left + " AND " + right;
+	}
+
+	private List<String> singletonList(String value) {
+		List<String> list = new ArrayList<>();
+		list.add(value);
+		return list;
+	}
+
+	private String buildCurrentOrgFilter() {
+		if (!erpProperties.isFilterCurrentOrg()) {
+			return "";
+		}
+		if (StringUtils.isNotBlank(erpProperties.getOrgNumber())) {
+			return "FPrdOrgId.FNUMBER='" + erpProperties.getOrgNumber().trim().replace("'", "''") + "'";
+		}
+		Long currentOrganizationId = erpClient.getCurrentOrganizationId();
+		if (currentOrganizationId == null) {
+			return "1=1";
+		}
+		return "FPrdOrgId=" + currentOrganizationId;
+	}
+
+	private String buildDetailFieldKeys() {
+		return DETAIL_FIELD_KEYS;
+	}
+
+	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 businessStatusName(String status) {
+		if ("1".equals(status)) {
+			return "计划";
+		}
+		if ("2".equals(status)) {
+			return "计划确认";
+		}
+		if ("3".equals(status)) {
+			return "下达";
+		}
+		if ("4".equals(status)) {
+			return "开工";
+		}
+		if ("5".equals(status)) {
+			return "完工";
+		}
+		if ("6".equals(status)) {
+			return "结案";
+		}
+		return status;
+	}
+}

+ 135 - 0
src/main/java/com/jeesite/modules/mes/erp/KingdeeErpClient.java

@@ -0,0 +1,135 @@
+package com.jeesite.modules.mes.erp;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.jeesite.common.lang.StringUtils;
+import okhttp3.Cookie;
+import okhttp3.CookieJar;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class KingdeeErpClient {
+
+	private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
+	private static final String LOGIN_PATH = "/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc";
+	private static final String BILL_QUERY_PATH = "/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.BillQuery.common.kdsvc";
+	private static final String VIEW_PATH = "/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.View.common.kdsvc";
+
+	private final KingdeeErpProperties properties;
+	private final MemoryCookieJar cookieJar = new MemoryCookieJar();
+	private final OkHttpClient httpClient;
+	private volatile long loginTimeMillis;
+	private volatile Long currentOrganizationId;
+
+	public KingdeeErpClient(KingdeeErpProperties properties) {
+		this.properties = properties;
+		this.httpClient = new OkHttpClient.Builder()
+				.cookieJar(cookieJar)
+				.connectTimeout(15, TimeUnit.SECONDS)
+				.readTimeout(30, TimeUnit.SECONDS)
+				.build();
+	}
+
+	public synchronized void loginIfNecessary() {
+		if (StringUtils.isBlank(properties.getUsername()) || StringUtils.isBlank(properties.getPassword())) {
+			throw new IllegalStateException("请先配置 ERP 登录账号:erp.k3cloud.username / erp.k3cloud.password");
+		}
+		if (!cookieJar.isEmpty() && System.currentTimeMillis() - loginTimeMillis < TimeUnit.MINUTES.toMillis(25)) {
+			return;
+		}
+		JSONObject body = new JSONObject();
+		body.put("acctID", properties.getAcctId());
+		body.put("username", properties.getUsername());
+		body.put("password", properties.getPassword());
+		body.put("lcid", properties.getLcid());
+
+		JSONObject result = postObject(LOGIN_PATH, body.toJSONString());
+		Integer loginResultType = result.getInteger("LoginResultType");
+		Boolean successByApi = result.getBoolean("IsSuccessByAPI");
+		if (loginResultType == null || loginResultType != 1 || !Boolean.TRUE.equals(successByApi)) {
+			throw new IllegalStateException("ERP 登录失败:" + result.toJSONString());
+		}
+		JSONObject context = result.getJSONObject("Context");
+		JSONObject currentOrganizationInfo = context == null ? null : context.getJSONObject("CurrentOrganizationInfo");
+		currentOrganizationId = currentOrganizationInfo == null ? null : currentOrganizationInfo.getLong("ID");
+		loginTimeMillis = System.currentTimeMillis();
+	}
+
+	public JSONArray billQuery(JSONObject data) {
+		loginIfNecessary();
+		JSONObject body = new JSONObject();
+		body.put("data", data);
+		String response = post(BILL_QUERY_PATH, body.toJSONString());
+		String trimmed = response == null ? "" : response.trim();
+		if (!trimmed.startsWith("[")) {
+			throw new IllegalStateException("ERP BillQuery 返回异常:" + response);
+		}
+		return JSON.parseArray(response);
+	}
+
+	public JSONObject view(String formId, JSONObject data) {
+		loginIfNecessary();
+		JSONObject body = new JSONObject();
+		body.put("formid", formId);
+		body.put("data", data.toJSONString());
+		return postObject(VIEW_PATH, body.toJSONString());
+	}
+
+	private JSONObject postObject(String path, String json) {
+		return JSON.parseObject(post(path, json));
+	}
+
+	private String post(String path, String json) {
+		Request request = new Request.Builder()
+				.url(properties.getBaseUrl() + path)
+				.post(RequestBody.create(JSON_MEDIA_TYPE, json))
+				.build();
+		try (Response response = httpClient.newCall(request).execute()) {
+			String body = response.body() == null ? "" : response.body().string();
+			if (!response.isSuccessful()) {
+				throw new IllegalStateException("ERP 请求失败,HTTP " + response.code() + ":" + body);
+			}
+			return body;
+		} catch (IOException e) {
+			throw new IllegalStateException("ERP 请求异常:" + e.getMessage(), e);
+		}
+	}
+
+	public Long getCurrentOrganizationId() {
+		loginIfNecessary();
+		return currentOrganizationId;
+	}
+
+	private static class MemoryCookieJar implements CookieJar {
+		private final Map<String, List<Cookie>> cookies = new HashMap<>();
+
+		@Override
+		public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
+			this.cookies.put(url.host(), new ArrayList<>(cookies));
+		}
+
+		@Override
+		public List<Cookie> loadForRequest(HttpUrl url) {
+			List<Cookie> hostCookies = cookies.get(url.host());
+			return hostCookies == null ? new ArrayList<>() : hostCookies;
+		}
+
+		private boolean isEmpty() {
+			return cookies.isEmpty();
+		}
+	}
+}

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

@@ -0,0 +1,74 @@
+package com.jeesite.modules.mes.erp;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class KingdeeErpProperties {
+
+	@Value("${erp.k3cloud.base-url:https://erp.anhuidayang.com:8800/k3cloud}")
+	private String baseUrl;
+
+	@Value("${erp.k3cloud.acct-id:672630265043d5}")
+	private String acctId;
+
+	@Value("${erp.k3cloud.username:}")
+	private String username;
+
+	@Value("${erp.k3cloud.password:}")
+	private String password;
+
+	@Value("${erp.k3cloud.org-number:100}")
+	private String orgNumber;
+
+	@Value("${erp.k3cloud.filter-current-org:false}")
+	private boolean filterCurrentOrg;
+
+	@Value("${erp.k3cloud.inventory-category-filter-enabled:false}")
+	private boolean inventoryCategoryFilterEnabled;
+
+	@Value("${erp.k3cloud.inventory-category-field:}")
+	private String inventoryCategoryField;
+
+	@Value("${erp.k3cloud.lcid:2052}")
+	private int lcid;
+
+	public String getBaseUrl() {
+		if (baseUrl.endsWith("/")) {
+			return baseUrl.substring(0, baseUrl.length() - 1);
+		}
+		return baseUrl;
+	}
+
+	public String getAcctId() {
+		return acctId;
+	}
+
+	public String getUsername() {
+		return username;
+	}
+
+	public String getPassword() {
+		return password;
+	}
+
+	public String getOrgNumber() {
+		return orgNumber;
+	}
+
+	public boolean isFilterCurrentOrg() {
+		return filterCurrentOrg;
+	}
+
+	public boolean isInventoryCategoryFilterEnabled() {
+		return inventoryCategoryFilterEnabled;
+	}
+
+	public String getInventoryCategoryField() {
+		return inventoryCategoryField;
+	}
+
+	public int getLcid() {
+		return lcid;
+	}
+}

+ 92 - 0
src/main/java/com/jeesite/modules/mes/web/ErpProductionOrderController.java

@@ -0,0 +1,92 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.erp.ErpProductionMaterial;
+import com.jeesite.modules.mes.erp.ErpProductionMaterialService;
+import com.jeesite.modules.mes.erp.ErpProductionOrder;
+import com.jeesite.modules.mes.erp.ErpProductionOrderService;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+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.List;
+
+@Controller
+@RequestMapping(value = "${adminPath}/mes/erpProductionOrder")
+public class ErpProductionOrderController extends BaseController {
+
+	private final ErpProductionOrderService erpProductionOrderService;
+	private final ErpProductionMaterialService erpProductionMaterialService;
+
+	public ErpProductionOrderController(ErpProductionOrderService erpProductionOrderService,
+			ErpProductionMaterialService erpProductionMaterialService) {
+		this.erpProductionOrderService = erpProductionOrderService;
+		this.erpProductionMaterialService = erpProductionMaterialService;
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(ErpProductionOrder erpProductionOrder, Model model) {
+		model.addAttribute("erpProductionOrder", erpProductionOrder);
+		return "modules/mes/erpProductionOrderList";
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<ErpProductionOrder> listData(ErpProductionOrder erpProductionOrder,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<ErpProductionOrder> page = new Page<>(request, response);
+		int pageSize = page.getPageSize() > 0 ? page.getPageSize() : 20;
+		int pageNo = page.getPageNo() > 0 ? page.getPageNo() : 1;
+		int startRow = (pageNo - 1) * pageSize;
+		List<ErpProductionOrder> list = erpProductionOrderService.findPageList(erpProductionOrder.getBillNo(), startRow, pageSize);
+		page.setList(list);
+		page.setCount(startRow + list.size() + (list.size() == pageSize ? 1 : 0));
+		return page;
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "detail")
+	public String detail(ErpProductionOrder erpProductionOrder, Model model) {
+		model.addAttribute("erpProductionOrder", erpProductionOrder);
+		return "modules/mes/erpProductionOrderDetail";
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "detailData")
+	@ResponseBody
+	public Page<ErpProductionOrder> detailData(ErpProductionOrder erpProductionOrder,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<ErpProductionOrder> page = new Page<>(request, response);
+		List<ErpProductionOrder> list = erpProductionOrderService.findDetailList(erpProductionOrder.getBillNo());
+		page.setList(list);
+		page.setCount(list.size());
+		return page;
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "material")
+	public String material(ErpProductionOrder erpProductionOrder, Model model) {
+		model.addAttribute("erpProductionOrder", erpProductionOrder);
+		return "modules/mes/erpProductionMaterialList";
+	}
+
+	@RequiresPermissions("mes:erpProductionOrder:view")
+	@RequestMapping(value = "materialData")
+	@ResponseBody
+	public Page<ErpProductionMaterial> materialData(ErpProductionOrder erpProductionOrder,
+			HttpServletRequest request, HttpServletResponse response) {
+		Page<ErpProductionMaterial> page = new Page<>(request, response);
+		List<ErpProductionMaterial> list = erpProductionMaterialService.findByProductionOrderEntry(
+				erpProductionOrder.getBillNo(), erpProductionOrder.getEntryId());
+		page.setList(list);
+		page.setCount(list.size());
+		return page;
+	}
+}

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

@@ -32,6 +32,20 @@ adw300:
     store:
       enabled: true
 
+# Kingdee K3Cloud ERP WebAPI settings.
+# Fill password in the runtime environment or local config before using ERP query pages.
+erp:
+  k3cloud:
+    base-url: https://erp.anhuidayang.com:8800/k3cloud
+    acct-id: 672630265043d5
+    username: 经博
+    password: JingBo@030323
+    org-number: 110
+    filter-current-org: true
+    inventory-category-filter-enabled: true
+    inventory-category-field:
+    lcid: 2052
+
 #======================================#
 #========== Server settings ===========#
 #======================================#

+ 46 - 0
src/main/resources/views/modules/mes/erpProductionMaterialList.html

@@ -0,0 +1,46 @@
+<% 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-ul"></i> ${text('生产用料清单')}:${erpProductionOrder.billNo}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpProductionOrder/detail?billNo=${erpProductionOrder.billNo}" 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">
+			<#form:form id="searchForm" model="${erpProductionOrder}" action="${ctx}/mes/erpProductionOrder/materialData" 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:hidden path="entryId"/>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("用料清单")}', name:'ppbomNo', index:'ppbomNo', width:150, align:"center", frozen:true},
+		{header:'${text("序号")}', name:'seq', index:'seq', width:70, align:"center"},
+		{header:'${text("子项物料编码")}', name:'materialNumber', index:'materialNumber', width:220, align:"left"},
+		{header:'${text("子项物料名称")}', name:'materialName', index:'materialName', width:260, align:"left"},
+		{header:'${text("规格型号")}', name:'specification', index:'specification', width:220, align:"left"},
+		{header:'${text("应发数量")}', name:'mustQty', index:'mustQty', width:120, align:"right"},
+		{header:'${text("已领数量")}', name:'pickedQty', index:'pickedQty', width:120, align:"right"},
+		{header:'${text("未领数量")}', name:'noPickedQty', index:'noPickedQty', width:120, align:"right"},
+		{header:'${text("领料数量")}', name:'issueQty', index:'issueQty', width:120, align:"right"},
+		{header:'${text("单位")}', name:'unitName', index:'unitName', width:90, align:"center"},
+		{header:'${text("发料仓库")}', name:'stockName', index:'stockName', width:180, align:"left"},
+		{header:'${text("调出仓库")}', name:'sourceStockName', index:'sourceStockName', width:180, align:"left"},
+		{header:'${text("库存状态")}', name:'stockStatusName', index:'stockStatusName', width:100, align:"center"},
+		{header:'${text("批号")}', name:'lotText', index:'lotText', width:120, align:"center"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

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

@@ -0,0 +1,50 @@
+<% 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生产订单明细')}:${erpProductionOrder.billNo}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="${ctx}/mes/erpProductionOrder/list" class="btn btn-default" title="${text('返回')}"><i class="fa fa-mail-reply"></i> ${text('返回')}</a>
+				<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="${erpProductionOrder}" action="${ctx}/mes/erpProductionOrder/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:'actions', width:100, align:"center", frozen:true, formatter: function(val, obj, row){
+			return '<a href="${ctx}/mes/erpProductionOrder/material?billNo='+encodeURIComponent(row.billNo)+'&entryId='+encodeURIComponent(row.entryId)+'" class="btnList addTabPage" data-title="${text("生产用料清单")}">'+
+				'<i class="fa fa-list-ul"></i> ${text("用料")}</a>';
+		}},
+		{header:'${text("行号")}', name:'seq', index:'seq', width:80, align:"center", frozen:true},
+		{header:'${text("物料编码")}', name:'materialNumber', index:'materialNumber', width:220, align:"left"},
+		{header:'${text("物料名称")}', name:'materialName', index:'materialName', width:260, align:"left"},
+		{header:'${text("存货类别")}', name:'inventoryCategory', index:'inventoryCategory', width:120, align:"center"},
+		{header:'${text("规格型号")}', name:'specification', index:'specification', width:180, align:"left"},
+		{header:'${text("数量")}', name:'qty', index:'qty', width:110, align:"right"},
+		{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"},
+		{header:'${text("计划完工")}', name:'planFinishDate', index:'planFinishDate', width:150, align:"center"},
+		{header:'${text("入库仓库")}', name:'stockName', index:'stockName', width:140, align:"left"},
+		{header:'${text("BOM版本")}', name:'bomNumber', index:'bomNumber', width:140, align:"left"},
+		{header:'${text("工艺路线")}', name:'routingNumber', index:'routingNumber', width:140, align:"left"},
+		{header:'${text("项目编号")}', name:'projectNo', index:'projectNo', width:130, align:"left"},
+		{header:'${text("计划跟踪号")}', name:'mtoNo', index:'mtoNo', width:130, align:"left"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>

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

@@ -0,0 +1,51 @@
+<% 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-alt"></i> ${text('ERP生产订单查询')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${erpProductionOrder}" action="${ctx}/mes/erpProductionOrder/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="billNo" maxlength="100" class="form-control width-180"/>
+					</div>
+				</div>
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("生产订单号")}', name:'billNo', index:'billNo', width:180, align:"center", frozen:true, formatter: function(val){
+			return '<a href="${ctx}/mes/erpProductionOrder/detail?billNo='+encodeURIComponent(val)+'" class="addTabPage" data-title="${text("ERP生产订单明细")}">'+val+'</a>';
+		}},
+		{header:'${text("单据日期")}', name:'date', index:'date', width:150, align:"center"},
+		{header:'${text("单据状态")}', name:'documentStatusName', index:'documentStatusName', width:100, align:"center"},
+		{header:'${text("生产组织")}', name:'prdOrgName', index:'prdOrgName', width:180, align:"left"},
+		{header:'${text("存货类别")}', name:'inventoryCategory', index:'inventoryCategory', width:120, align:"center"},
+		{header:'${text("生产车间")}', name:'workshopName', index:'workshopName', width:140, align:"left"},
+		{header:'${text("计划员")}', name:'plannerName', index:'plannerName', width:120, align:"left"},
+		{header:'${text("明细行数")}', name:'entryCount', index:'entryCount', width:100, align:"right"},
+		{header:'${text("总数量")}', name:'totalQty', index:'totalQty', width:120, align:"right"}
+	],
+	ajaxSuccess: function(){}
+});
+</script>