|
|
@@ -16,6 +16,8 @@ import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
@Service
|
|
|
public class ErpProductionOrderService {
|
|
|
@@ -24,6 +26,14 @@ public class ErpProductionOrderService {
|
|
|
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 long VIEW_CATEGORY_CACHE_TTL_MS = TimeUnit.MINUTES.toMillis(10);
|
|
|
+ private static final int VIEW_PREFETCH_BATCH_SIZE = 8;
|
|
|
+ /** PRD_MO 明细「存货类别」View 返回字段 */
|
|
|
+ private static final String INVENTORY_CATEGORY_VIEW_FIELD = "F_XYLX_BaseProperty_jv4";
|
|
|
+
|
|
|
+ private final ConcurrentHashMap<String, ViewDetailCacheEntry> viewCategoryCache = new ConcurrentHashMap<>();
|
|
|
+ /** BillQuery 存货类别字段不可用时会自动降级为 View 兜底 */
|
|
|
+ private volatile boolean billQueryCategoryFieldDisabled = false;
|
|
|
private static final String HEADER_FIELD_KEYS = String.join(",",
|
|
|
"FID",
|
|
|
"FBILLNO",
|
|
|
@@ -40,12 +50,14 @@ public class ErpProductionOrderService {
|
|
|
"FDate",
|
|
|
"FPrdOrgId.FNAME",
|
|
|
"FWorkShopID0.FNAME",
|
|
|
+ "FWorkShopID.FNAME",
|
|
|
"FPlannerID.FNAME",
|
|
|
"FTreeEntity_FEntryId",
|
|
|
"FTreeEntity_FSeq",
|
|
|
"FMATERIALID.FNUMBER",
|
|
|
"FMATERIALID.FNAME",
|
|
|
"FSpecification",
|
|
|
+ "FMATERIALID.FSpecification",
|
|
|
"FQTY",
|
|
|
"FUnitId.FNAME",
|
|
|
"FPlanStartDate",
|
|
|
@@ -65,8 +77,102 @@ public class ErpProductionOrderService {
|
|
|
this.erpProperties = erpProperties;
|
|
|
}
|
|
|
|
|
|
+ /** 领料单列表等场景:取 MES 可见范围内的生产订单号 */
|
|
|
+ private static final int VISIBLE_BILL_NO_BATCH = 500;
|
|
|
+
|
|
|
+ public List<String> findVisibleBillNos(int limit) {
|
|
|
+ int fetchLimit = limit > 0 ? limit : VISIBLE_BILL_NO_BATCH;
|
|
|
+ List<ErpProductionOrder> orders = findPageList("", 0, fetchLimit);
|
|
|
+ List<String> billNos = new ArrayList<>();
|
|
|
+ for (ErpProductionOrder order : orders) {
|
|
|
+ if (order != null && StringUtils.isNotBlank(order.getBillNo())) {
|
|
|
+ billNos.add(order.getBillNo().trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return billNos;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isVisibleProductionOrder(String billNo) {
|
|
|
+ if (StringUtils.isBlank(billNo)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return !findPageList(billNo.trim(), 0, 1).isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
public List<ErpProductionOrder> findPageList(String billNo, int startRow, int limit) {
|
|
|
- List<ErpProductionOrder> headers = findFilteredHeaderPageList(billNo, startRow, limit);
|
|
|
+ if (!erpProperties.isInventoryCategoryFilterEnabled()) {
|
|
|
+ return findPageListWithDetailSummary(billNo, startRow, limit);
|
|
|
+ }
|
|
|
+ if (useBillQueryCategoryField()) {
|
|
|
+ try {
|
|
|
+ return findAggregatedPageListByBillQuery(billNo, startRow, limit);
|
|
|
+ } catch (IllegalStateException ex) {
|
|
|
+ if (disableBillQueryCategoryFieldIfInvalid(ex)) {
|
|
|
+ return findAggregatedPageListByView(billNo, startRow, limit);
|
|
|
+ }
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return findAggregatedPageListByView(billNo, startRow, limit);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ErpProductionOrder> findAggregatedPageListByBillQuery(String billNo, int startRow, int limit) {
|
|
|
+ int targetSize = startRow + limit;
|
|
|
+ int fetchStart = 0;
|
|
|
+ int fetchLimit = Math.max(limit * 3, 50);
|
|
|
+ int maxFetchRows = Math.max(targetSize * 5, 200);
|
|
|
+ Map<String, ErpProductionOrder> orderMap = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ while (orderMap.size() < targetSize && fetchStart < maxFetchRows) {
|
|
|
+ List<ErpProductionOrder> chunk = findDetailCandidateList(billNo, fetchStart, fetchLimit);
|
|
|
+ if (chunk.isEmpty()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ billQueryCategoryFieldDisabled = false;
|
|
|
+ for (ErpProductionOrder detail : chunk) {
|
|
|
+ if (StringUtils.isBlank(detail.getInventoryCategory())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ ErpProductionOrder order = orderMap.get(detail.getBillNo());
|
|
|
+ if (order == null) {
|
|
|
+ order = toHeaderFromDetail(detail);
|
|
|
+ order.setEntryCount(0);
|
|
|
+ order.setTotalQty(BigDecimal.ZERO);
|
|
|
+ orderMap.put(detail.getBillNo(), order);
|
|
|
+ }
|
|
|
+ order.setEntryCount(order.getEntryCount() + 1);
|
|
|
+ if (detail.getQty() != null) {
|
|
|
+ order.setTotalQty(order.getTotalQty().add(detail.getQty()));
|
|
|
+ }
|
|
|
+ mergeHeaderFieldsFromDetail(order, detail);
|
|
|
+ }
|
|
|
+ if (chunk.size() < fetchLimit) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ fetchStart += fetchLimit;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<ErpProductionOrder> allOrders = new ArrayList<>(orderMap.values());
|
|
|
+ if (startRow >= allOrders.size()) {
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+ return new ArrayList<>(allOrders.subList(startRow, Math.min(startRow + limit, allOrders.size())));
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ErpProductionOrder> findAggregatedPageListByView(String billNo, int startRow, int limit) {
|
|
|
+ Map<String, ViewDetailSummary> summaryCache = new LinkedHashMap<>();
|
|
|
+ List<ErpProductionOrder> headers = findFilteredHeaderPageListByView(billNo, startRow, limit, summaryCache);
|
|
|
+ for (ErpProductionOrder header : headers) {
|
|
|
+ ViewDetailSummary summary = getViewDetailSummary(header.getBillNo(), summaryCache);
|
|
|
+ header.setEntryCount(summary.entryCount);
|
|
|
+ header.setTotalQty(summary.totalQty);
|
|
|
+ mergeHeaderFieldsFromSummary(header, summary);
|
|
|
+ }
|
|
|
+ return headers;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ErpProductionOrder> findPageListWithDetailSummary(String billNo, int startRow, int limit) {
|
|
|
+ List<ErpProductionOrder> headers = findHeaderPageList(billNo, startRow, limit);
|
|
|
if (headers.isEmpty()) {
|
|
|
return headers;
|
|
|
}
|
|
|
@@ -83,25 +189,74 @@ public class ErpProductionOrderService {
|
|
|
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()));
|
|
|
}
|
|
|
+ mergeHeaderFieldsFromDetail(order, detail);
|
|
|
}
|
|
|
return new ArrayList<>(orderMap.values());
|
|
|
}
|
|
|
|
|
|
- private List<ErpProductionOrder> findFilteredHeaderPageList(String billNo, int startRow, int limit) {
|
|
|
- if (!erpProperties.isInventoryCategoryFilterEnabled()) {
|
|
|
- return findHeaderPageList(billNo, startRow, limit);
|
|
|
+ private boolean useBillQueryCategoryField() {
|
|
|
+ return !billQueryCategoryFieldDisabled
|
|
|
+ && erpProperties.isInventoryCategoryFilterEnabled()
|
|
|
+ && StringUtils.isNotBlank(erpProperties.getInventoryCategoryField());
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean disableBillQueryCategoryFieldIfInvalid(IllegalStateException ex) {
|
|
|
+ if (!isBillQueryCategoryFieldError(ex)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ billQueryCategoryFieldDisabled = true;
|
|
|
+ logger.warn("ERP BillQuery 存货类别字段 {} 不可用,已切换为 View 兜底:{}",
|
|
|
+ erpProperties.getInventoryCategoryField(), ex.getMessage());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isBillQueryCategoryFieldError(IllegalStateException ex) {
|
|
|
+ String message = ex.getMessage();
|
|
|
+ return message != null && message.contains("不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONArray executeDetailBillQuery(JSONObject data, boolean strictCategoryField) {
|
|
|
+ try {
|
|
|
+ return erpClient.billQuery(data);
|
|
|
+ } catch (IllegalStateException ex) {
|
|
|
+ String categoryField = erpProperties.getInventoryCategoryField();
|
|
|
+ if (disableBillQueryCategoryFieldIfInvalid(ex)) {
|
|
|
+ if (strictCategoryField) {
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+ data.put("FieldKeys", DETAIL_FIELD_KEYS);
|
|
|
+ data.put("FilterString", stripCategoryFilter(data.getString("FilterString"), categoryField));
|
|
|
+ return erpClient.billQuery(data);
|
|
|
+ }
|
|
|
+ throw ex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String stripCategoryFilter(String filterString, String categoryField) {
|
|
|
+ if (StringUtils.isBlank(filterString) || StringUtils.isBlank(categoryField)) {
|
|
|
+ return filterString;
|
|
|
}
|
|
|
+ String escaped = java.util.regex.Pattern.quote(categoryField.trim());
|
|
|
+ String rebuilt = filterString.replaceAll("\\s*AND\\s*\\(" + escaped + "=[^)]*\\)", "")
|
|
|
+ .replaceAll("\\(" + escaped + "=[^)]*\\)\\s*AND\\s*", "")
|
|
|
+ .replaceAll("\\(" + escaped + "=[^)]*\\)", "")
|
|
|
+ .trim();
|
|
|
+ if (rebuilt.startsWith("AND ")) {
|
|
|
+ rebuilt = rebuilt.substring(4).trim();
|
|
|
+ }
|
|
|
+ return StringUtils.isBlank(rebuilt) ? buildCurrentOrgFilter() : rebuilt;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ErpProductionOrder> findFilteredHeaderPageListByView(String billNo, int startRow, int limit,
|
|
|
+ Map<String, ViewDetailSummary> summaryCache) {
|
|
|
int targetSize = startRow + limit;
|
|
|
int fetchStart = 0;
|
|
|
int fetchLimit = Math.max(limit * 5, 50);
|
|
|
- int maxFetchRows = Math.max(targetSize * 30, 500);
|
|
|
+ int maxFetchRows = Math.max(targetSize * 20, 300);
|
|
|
List<ErpProductionOrder> matchedHeaders = new ArrayList<>();
|
|
|
|
|
|
while (matchedHeaders.size() < targetSize && fetchStart < maxFetchRows) {
|
|
|
@@ -109,12 +264,16 @@ public class ErpProductionOrderService {
|
|
|
if (candidates.isEmpty()) {
|
|
|
break;
|
|
|
}
|
|
|
+ prefetchViewDetailSummaries(candidates, summaryCache);
|
|
|
for (ErpProductionOrder header : candidates) {
|
|
|
- String category = findAllowedInventoryCategoryByView(header.getBillNo());
|
|
|
- if (StringUtils.isBlank(category)) {
|
|
|
+ ViewDetailSummary summary = getViewDetailSummary(header.getBillNo(), summaryCache);
|
|
|
+ if (summary.entryCount <= 0) {
|
|
|
continue;
|
|
|
}
|
|
|
- header.setInventoryCategory(category);
|
|
|
+ if (!summary.categories.isEmpty()) {
|
|
|
+ header.setInventoryCategory(summary.categories.values().iterator().next());
|
|
|
+ }
|
|
|
+ mergeHeaderFieldsFromSummary(header, summary);
|
|
|
matchedHeaders.add(header);
|
|
|
if (matchedHeaders.size() >= targetSize) {
|
|
|
break;
|
|
|
@@ -133,31 +292,7 @@ public class ErpProductionOrderService {
|
|
|
}
|
|
|
|
|
|
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())));
|
|
|
+ return findHeaderList(billNo, startRow, limit);
|
|
|
}
|
|
|
|
|
|
private List<ErpProductionOrder> findDetailCandidateList(String billNo, int startRow, int limit) {
|
|
|
@@ -170,7 +305,7 @@ public class ErpProductionOrderService {
|
|
|
data.put("Limit", Math.max(limit, 1));
|
|
|
logger.info("ERP production order candidate query: {}", data.toJSONString());
|
|
|
|
|
|
- JSONArray rows = erpClient.billQuery(data);
|
|
|
+ JSONArray rows = executeDetailBillQuery(data, useBillQueryCategoryField());
|
|
|
List<ErpProductionOrder> list = new ArrayList<>();
|
|
|
for (int i = 0; i < rows.size(); i++) {
|
|
|
list.add(toDetail(rows.getJSONObject(i), i));
|
|
|
@@ -212,14 +347,20 @@ public class ErpProductionOrderService {
|
|
|
data.put("Limit", 2000);
|
|
|
logger.info("ERP production order detail query: {}", data.toJSONString());
|
|
|
|
|
|
- JSONArray rows = erpClient.billQuery(data);
|
|
|
+ JSONArray rows = executeDetailBillQuery(data, false);
|
|
|
List<ErpProductionOrder> list = new ArrayList<>();
|
|
|
- Map<String, Map<String, String>> categoryCache = new LinkedHashMap<>();
|
|
|
+ boolean useBillQueryCategory = useBillQueryCategoryField();
|
|
|
+ boolean useViewCategory = erpProperties.isInventoryCategoryFilterEnabled() && !useBillQueryCategory;
|
|
|
+ Map<String, ViewDetailSummary> summaryCache = new LinkedHashMap<>();
|
|
|
+ if (useViewCategory) {
|
|
|
+ prefetchViewDetailSummariesByBillNos(billNos, summaryCache);
|
|
|
+ }
|
|
|
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 (useViewCategory) {
|
|
|
+ detail.setInventoryCategory(getViewDetailSummary(detail.getBillNo(), summaryCache)
|
|
|
+ .categories.get(detail.getEntryId()));
|
|
|
+ }
|
|
|
if (!erpProperties.isInventoryCategoryFilterEnabled()
|
|
|
|| StringUtils.isNotBlank(detail.getInventoryCategory())) {
|
|
|
list.add(detail);
|
|
|
@@ -235,9 +376,9 @@ public class ErpProductionOrderService {
|
|
|
order.setBillNo(row.getString("FBILLNO"));
|
|
|
order.setDocumentStatus(row.getString("FDocumentStatus"));
|
|
|
order.setDocumentStatusName(documentStatusName(order.getDocumentStatus()));
|
|
|
- order.setDate(row.getString("FDate"));
|
|
|
+ order.setDate(formatErpDate(row.getString("FDate")));
|
|
|
order.setPrdOrgName(row.getString("FPrdOrgId.FNAME"));
|
|
|
- order.setWorkshopName(row.getString("FWorkShopID0.FNAME"));
|
|
|
+ order.setWorkshopName(readWorkshopName(row));
|
|
|
order.setPlannerName(row.getString("FPlannerID.FNAME"));
|
|
|
return order;
|
|
|
}
|
|
|
@@ -266,17 +407,17 @@ public class ErpProductionOrderService {
|
|
|
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.setDate(formatErpDate(row.getString("FDate")));
|
|
|
order.setPrdOrgName(row.getString("FPrdOrgId.FNAME"));
|
|
|
- order.setWorkshopName(row.getString("FWorkShopID0.FNAME"));
|
|
|
+ order.setWorkshopName(readWorkshopName(row));
|
|
|
order.setPlannerName(row.getString("FPlannerID.FNAME"));
|
|
|
order.setMaterialNumber(row.getString("FMATERIALID.FNUMBER"));
|
|
|
order.setMaterialName(row.getString("FMATERIALID.FNAME"));
|
|
|
- order.setSpecification(row.getString("FSpecification"));
|
|
|
+ order.setSpecification(readSpecification(row));
|
|
|
order.setQty(row.getBigDecimal("FQTY"));
|
|
|
order.setUnitName(row.getString("FUnitId.FNAME"));
|
|
|
- order.setPlanStartDate(row.getString("FPlanStartDate"));
|
|
|
- order.setPlanFinishDate(row.getString("FPlanFinishDate"));
|
|
|
+ order.setPlanStartDate(formatErpDate(row.getString("FPlanStartDate")));
|
|
|
+ order.setPlanFinishDate(formatErpDate(row.getString("FPlanFinishDate")));
|
|
|
order.setBusinessStatus(row.getString("FStatus"));
|
|
|
order.setBusinessStatusName(businessStatusName(order.getBusinessStatus()));
|
|
|
order.setStockName(row.getString("FStockId.FNAME"));
|
|
|
@@ -284,20 +425,79 @@ public class ErpProductionOrderService {
|
|
|
order.setRoutingNumber(row.getString("FRoutingId.FNUMBER"));
|
|
|
order.setProjectNo(row.getString("FProjectNo"));
|
|
|
order.setMtoNo(row.getString("FMTONO"));
|
|
|
+ String categoryField = erpProperties.getInventoryCategoryField();
|
|
|
+ if (StringUtils.isNotBlank(categoryField)) {
|
|
|
+ order.setInventoryCategory(normalizeInventoryCategory(row.getString(categoryField.trim())));
|
|
|
+ }
|
|
|
return order;
|
|
|
}
|
|
|
|
|
|
- private String findAllowedInventoryCategoryByView(String billNo) {
|
|
|
- Map<String, String> categories = findAllowedInventoryCategoriesByView(billNo);
|
|
|
- if (categories.isEmpty()) {
|
|
|
+ private String normalizeInventoryCategory(String rawValue) {
|
|
|
+ if (StringUtils.isBlank(rawValue)) {
|
|
|
return "";
|
|
|
}
|
|
|
- return categories.values().iterator().next();
|
|
|
+ String trimmed = rawValue.trim();
|
|
|
+ if (isAllowedCategory(trimmed)) {
|
|
|
+ return trimmed;
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ private void prefetchViewDetailSummaries(List<ErpProductionOrder> headers,
|
|
|
+ Map<String, ViewDetailSummary> summaryCache) {
|
|
|
+ List<String> billNos = new ArrayList<>();
|
|
|
+ for (ErpProductionOrder header : headers) {
|
|
|
+ if (header != null && StringUtils.isNotBlank(header.getBillNo())) {
|
|
|
+ billNos.add(header.getBillNo());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ prefetchViewDetailSummariesByBillNos(billNos, summaryCache);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void prefetchViewDetailSummariesByBillNos(List<String> billNos, Map<String, ViewDetailSummary> summaryCache) {
|
|
|
+ List<String> pendingBillNos = new ArrayList<>();
|
|
|
+ for (String billNo : billNos) {
|
|
|
+ if (StringUtils.isBlank(billNo) || summaryCache.containsKey(billNo)
|
|
|
+ || hasValidViewDetailCache(billNo)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ pendingBillNos.add(billNo);
|
|
|
+ }
|
|
|
+ for (int i = 0; i < pendingBillNos.size(); i += VIEW_PREFETCH_BATCH_SIZE) {
|
|
|
+ int end = Math.min(i + VIEW_PREFETCH_BATCH_SIZE, pendingBillNos.size());
|
|
|
+ List<String> batch = pendingBillNos.subList(i, end);
|
|
|
+ batch.parallelStream().forEach(billNo -> getViewDetailSummary(billNo, summaryCache));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private Map<String, String> findAllowedInventoryCategoriesByView(String billNo) {
|
|
|
+ private ViewDetailSummary getViewDetailSummary(String billNo, Map<String, ViewDetailSummary> summaryCache) {
|
|
|
if (StringUtils.isBlank(billNo)) {
|
|
|
- return Collections.emptyMap();
|
|
|
+ return ViewDetailSummary.empty();
|
|
|
+ }
|
|
|
+ ViewDetailSummary cachedInRequest = summaryCache.get(billNo);
|
|
|
+ if (cachedInRequest != null) {
|
|
|
+ return cachedInRequest;
|
|
|
+ }
|
|
|
+ ViewDetailCacheEntry cachedEntry = viewCategoryCache.get(billNo);
|
|
|
+ if (cachedEntry != null && cachedEntry.expireAt > System.currentTimeMillis()) {
|
|
|
+ summaryCache.put(billNo, cachedEntry.summary);
|
|
|
+ return cachedEntry.summary;
|
|
|
+ }
|
|
|
+ ViewDetailSummary summary = loadViewDetailSummary(billNo);
|
|
|
+ viewCategoryCache.put(billNo, new ViewDetailCacheEntry(
|
|
|
+ System.currentTimeMillis() + VIEW_CATEGORY_CACHE_TTL_MS, summary));
|
|
|
+ summaryCache.put(billNo, summary);
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasValidViewDetailCache(String billNo) {
|
|
|
+ ViewDetailCacheEntry cachedEntry = viewCategoryCache.get(billNo);
|
|
|
+ return cachedEntry != null && cachedEntry.expireAt > System.currentTimeMillis();
|
|
|
+ }
|
|
|
+
|
|
|
+ private ViewDetailSummary loadViewDetailSummary(String billNo) {
|
|
|
+ if (StringUtils.isBlank(billNo)) {
|
|
|
+ return ViewDetailSummary.empty();
|
|
|
}
|
|
|
JSONObject data = new JSONObject();
|
|
|
data.put("CreateOrgId", 0);
|
|
|
@@ -309,40 +509,58 @@ public class ErpProductionOrderService {
|
|
|
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();
|
|
|
+ return ViewDetailSummary.empty();
|
|
|
}
|
|
|
JSONObject result = resultWrapper.getJSONObject("Result");
|
|
|
if (result == null) {
|
|
|
- return Collections.emptyMap();
|
|
|
+ return ViewDetailSummary.empty();
|
|
|
}
|
|
|
JSONArray treeEntity = result.getJSONArray("TreeEntity");
|
|
|
if (treeEntity == null) {
|
|
|
- return Collections.emptyMap();
|
|
|
+ return ViewDetailSummary.empty();
|
|
|
}
|
|
|
- Map<String, String> categories = new LinkedHashMap<>();
|
|
|
+ ViewDetailSummary summary = new ViewDetailSummary();
|
|
|
+ summary.plannerName = getNameValue(result.getJSONObject("PlannerId"));
|
|
|
+ summary.workshopName = getNameValue(result.getJSONObject("WorkShopId0"));
|
|
|
for (int i = 0; i < treeEntity.size(); i++) {
|
|
|
JSONObject entry = treeEntity.getJSONObject(i);
|
|
|
+ if (StringUtils.isBlank(summary.workshopName)) {
|
|
|
+ summary.workshopName = getNameValue(entry.getJSONObject("WorkShopId"));
|
|
|
+ }
|
|
|
String category = extractAllowedCategory(entry);
|
|
|
- if (StringUtils.isNotBlank(category)) {
|
|
|
- categories.put(entry.getString("Id"), category);
|
|
|
+ if (StringUtils.isBlank(category)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ summary.categories.put(entry.getString("Id"), category);
|
|
|
+ summary.entryCount++;
|
|
|
+ BigDecimal qty = entry.getBigDecimal("Qty");
|
|
|
+ if (qty == null) {
|
|
|
+ qty = entry.getBigDecimal("FQty");
|
|
|
+ }
|
|
|
+ if (qty != null) {
|
|
|
+ summary.totalQty = summary.totalQty.add(qty);
|
|
|
}
|
|
|
}
|
|
|
- return categories;
|
|
|
+ return summary;
|
|
|
}
|
|
|
|
|
|
private String extractAllowedCategory(JSONObject entry) {
|
|
|
if (entry == null) {
|
|
|
return "";
|
|
|
}
|
|
|
+ String category = extractAllowedCategoryFromBaseProperty(entry.get(INVENTORY_CATEGORY_VIEW_FIELD));
|
|
|
+ if (StringUtils.isNotBlank(category)) {
|
|
|
+ return category;
|
|
|
+ }
|
|
|
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);
|
|
|
+ JSONObject categoryObj = materialBase.getJSONObject(i).getJSONObject("CategoryID");
|
|
|
+ String number = categoryObj == null ? "" : categoryObj.getString("Number");
|
|
|
+ String name = getNameValue(categoryObj);
|
|
|
if (isAllowedCategory(number)) {
|
|
|
return number;
|
|
|
}
|
|
|
@@ -353,6 +571,29 @@ public class ErpProductionOrderService {
|
|
|
return "";
|
|
|
}
|
|
|
|
|
|
+ private String extractAllowedCategoryFromBaseProperty(Object propertyValue) {
|
|
|
+ if (propertyValue == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ if (propertyValue instanceof String) {
|
|
|
+ String text = ((String) propertyValue).trim();
|
|
|
+ return isAllowedCategory(text) ? text : "";
|
|
|
+ }
|
|
|
+ if (!(propertyValue instanceof JSONObject)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ JSONObject baseProperty = (JSONObject) propertyValue;
|
|
|
+ String number = StringUtils.defaultIfBlank(baseProperty.getString("Number"), baseProperty.getString("FNumber"));
|
|
|
+ if (isAllowedCategory(number)) {
|
|
|
+ return number.trim();
|
|
|
+ }
|
|
|
+ String name = getNameValue(baseProperty);
|
|
|
+ if (isAllowedCategory(name)) {
|
|
|
+ return name;
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
private String getNameValue(JSONObject object) {
|
|
|
if (object == null) {
|
|
|
return "";
|
|
|
@@ -364,6 +605,56 @@ public class ErpProductionOrderService {
|
|
|
return nameArray.getJSONObject(0).getString("Value");
|
|
|
}
|
|
|
|
|
|
+ private String firstNonBlank(String... values) {
|
|
|
+ if (values == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ for (String value : values) {
|
|
|
+ if (StringUtils.isNotBlank(value)) {
|
|
|
+ return value.trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ /** BillQuery 明细行优先取 FWorkShopID,表头 FWorkShopID0 兜底 */
|
|
|
+ private String readWorkshopName(JSONObject row) {
|
|
|
+ return firstNonBlank(
|
|
|
+ row.getString("FWorkShopID.FNAME"),
|
|
|
+ row.getString("FWorkShopID0.FNAME"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 明细规格优先取行字段,物料主数据规格兜底 */
|
|
|
+ private String readSpecification(JSONObject row) {
|
|
|
+ return firstNonBlank(
|
|
|
+ row.getString("FSpecification"),
|
|
|
+ row.getString("FMATERIALID.FSpecification"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void mergeHeaderFieldsFromDetail(ErpProductionOrder header, ErpProductionOrder detail) {
|
|
|
+ if (header == null || detail == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(header.getWorkshopName())) {
|
|
|
+ header.setWorkshopName(detail.getWorkshopName());
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(header.getPlannerName())) {
|
|
|
+ header.setPlannerName(detail.getPlannerName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void mergeHeaderFieldsFromSummary(ErpProductionOrder header, ViewDetailSummary summary) {
|
|
|
+ if (header == null || summary == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(header.getWorkshopName())) {
|
|
|
+ header.setWorkshopName(summary.workshopName);
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(header.getPlannerName())) {
|
|
|
+ header.setPlannerName(summary.plannerName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private boolean isAllowedCategory(String value) {
|
|
|
if (StringUtils.isBlank(value)) {
|
|
|
return false;
|
|
|
@@ -372,16 +663,27 @@ public class ErpProductionOrderService {
|
|
|
}
|
|
|
|
|
|
private String buildFilterString(String billNo) {
|
|
|
- String orgFilter = buildCurrentOrgFilter();
|
|
|
- String baseFilter = orgFilter;
|
|
|
+ String baseFilter = appendAnd(buildCurrentOrgFilter(), buildInventoryCategoryFilter());
|
|
|
if (StringUtils.isBlank(billNo)) {
|
|
|
return baseFilter;
|
|
|
}
|
|
|
return appendAnd(baseFilter, "FBILLNO='" + billNo.trim().replace("'", "''") + "'");
|
|
|
}
|
|
|
|
|
|
+ private String buildInventoryCategoryFilter() {
|
|
|
+ if (!useBillQueryCategoryField()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ String categoryField = erpProperties.getInventoryCategoryField();
|
|
|
+ List<String> parts = new ArrayList<>();
|
|
|
+ for (String category : ALLOWED_INVENTORY_CATEGORIES) {
|
|
|
+ parts.add(categoryField.trim() + "='" + category.replace("'", "''") + "'");
|
|
|
+ }
|
|
|
+ return "(" + String.join(" OR ", parts) + ")";
|
|
|
+ }
|
|
|
+
|
|
|
private String buildInFilterString(List<String> billNos) {
|
|
|
- String baseFilter = buildCurrentOrgFilter();
|
|
|
+ String baseFilter = appendAnd(buildCurrentOrgFilter(), buildInventoryCategoryFilter());
|
|
|
StringBuilder builder = new StringBuilder(baseFilter);
|
|
|
if (builder.length() > 0) {
|
|
|
builder.append(" AND ");
|
|
|
@@ -428,7 +730,40 @@ public class ErpProductionOrderService {
|
|
|
}
|
|
|
|
|
|
private String buildDetailFieldKeys() {
|
|
|
- return DETAIL_FIELD_KEYS;
|
|
|
+ if (!useBillQueryCategoryField()) {
|
|
|
+ return DETAIL_FIELD_KEYS;
|
|
|
+ }
|
|
|
+ return DETAIL_FIELD_KEYS + "," + erpProperties.getInventoryCategoryField().trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ViewDetailSummary {
|
|
|
+ private final Map<String, String> categories = new LinkedHashMap<>();
|
|
|
+ private int entryCount;
|
|
|
+ private BigDecimal totalQty = BigDecimal.ZERO;
|
|
|
+ private String workshopName = "";
|
|
|
+ private String plannerName = "";
|
|
|
+
|
|
|
+ private static ViewDetailSummary empty() {
|
|
|
+ return new ViewDetailSummary();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ViewDetailCacheEntry {
|
|
|
+ private final long expireAt;
|
|
|
+ private final ViewDetailSummary summary;
|
|
|
+
|
|
|
+ private ViewDetailCacheEntry(long expireAt, ViewDetailSummary summary) {
|
|
|
+ this.expireAt = expireAt;
|
|
|
+ this.summary = summary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) {
|