ErpPickMaterialBillService.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. package com.jeesite.modules.mes.erp;
  2. import com.alibaba.fastjson.JSONArray;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.jeesite.common.lang.StringUtils;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.stereotype.Service;
  8. import java.math.BigDecimal;
  9. import java.util.ArrayList;
  10. import java.util.LinkedHashMap;
  11. import java.util.List;
  12. import java.util.Map;
  13. /**
  14. * ERP 生产领料单列表 BillQuery(PRD_PickMtrl)。
  15. */
  16. @Service
  17. public class ErpPickMaterialBillService {
  18. private static final Logger logger = LoggerFactory.getLogger(ErpPickMaterialBillService.class);
  19. private static final String FORM_ID = "PRD_PickMtrl";
  20. private static final int VISIBLE_MO_LIMIT = 500;
  21. /** BillQuery 分录字段:除分录内码外,明细字段一般不带 FEntity_ 前缀 */
  22. private static final String MO_BILL_NO_FIELD = "FMoBillNo";
  23. private static final String MATERIAL_NUMBER_FIELD = "FMaterialId.FNUMBER";
  24. private static final String HEADER_FIELD_KEYS = String.join(",",
  25. "FID",
  26. "FBILLNO",
  27. "FDate",
  28. "FDocumentStatus",
  29. "FPrdOrgId.FNUMBER",
  30. "FPrdOrgId.FNAME",
  31. "FStockOrgId.FNUMBER",
  32. "FStockOrgId.FNAME");
  33. private static final String DETAIL_FIELD_KEYS = String.join(",",
  34. "FID",
  35. "FBILLNO",
  36. "FDate",
  37. "FDocumentStatus",
  38. "FPrdOrgId.FNUMBER",
  39. "FPrdOrgId.FNAME",
  40. "FStockOrgId.FNUMBER",
  41. "FStockOrgId.FNAME",
  42. "FEntryWorkShopId.FNUMBER",
  43. "FEntryWorkShopId.FNAME",
  44. "FEntity_FEntryID",
  45. "FEntity_FSeq",
  46. "FMoEntrySeq",
  47. MATERIAL_NUMBER_FIELD,
  48. "FMaterialName",
  49. "FUnitID.FNAME",
  50. "FAppQty",
  51. "FActualQty",
  52. MO_BILL_NO_FIELD,
  53. "FPPBomBillNo",
  54. "FStockId.FNUMBER",
  55. "FStockId.FNAME");
  56. private final KingdeeErpClient erpClient;
  57. private final KingdeeErpProperties erpProperties;
  58. private final ErpProductionOrderService erpProductionOrderService;
  59. private final MesErpPickKcService mesErpPickKcService;
  60. public ErpPickMaterialBillService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties,
  61. ErpProductionOrderService erpProductionOrderService, MesErpPickKcService mesErpPickKcService) {
  62. this.erpClient = erpClient;
  63. this.erpProperties = erpProperties;
  64. this.erpProductionOrderService = erpProductionOrderService;
  65. this.mesErpPickKcService = mesErpPickKcService;
  66. }
  67. /**
  68. * 列表页:按领料单号聚合。
  69. */
  70. public List<ErpPickMaterialBill> findPageList(ErpPickMaterialBill query, int startRow, int limit) {
  71. String moFilter = buildProductionOrderFilter(query);
  72. if (moFilter == null) {
  73. return new ArrayList<>();
  74. }
  75. if (StringUtils.isNotBlank(query == null ? "" : query.getMaterialNumberQuery())) {
  76. return findAggregatedPageList(query, moFilter, startRow, limit);
  77. }
  78. List<ErpPickMaterialBill> headers = findHeaderList(query, moFilter, startRow, limit);
  79. if (headers.isEmpty()) {
  80. return headers;
  81. }
  82. List<String> billNos = new ArrayList<>();
  83. Map<String, ErpPickMaterialBill> billMap = new LinkedHashMap<>();
  84. for (ErpPickMaterialBill header : headers) {
  85. header.setEntryCount(0);
  86. header.setTotalAppQty(BigDecimal.ZERO);
  87. header.setTotalActualQty(BigDecimal.ZERO);
  88. billMap.put(header.getBillNo(), header);
  89. billNos.add(header.getBillNo());
  90. }
  91. for (ErpPickMaterialBill detail : findDetailListByBillNos(billNos, moFilter)) {
  92. ErpPickMaterialBill header = billMap.get(detail.getBillNo());
  93. if (header == null) {
  94. continue;
  95. }
  96. if (StringUtils.isBlank(header.getMoBillNo()) && StringUtils.isNotBlank(detail.getMoBillNo())) {
  97. header.setMoBillNo(detail.getMoBillNo());
  98. }
  99. if (StringUtils.isBlank(header.getWorkshopName()) && StringUtils.isNotBlank(detail.getWorkshopName())) {
  100. header.setWorkshopNumber(detail.getWorkshopNumber());
  101. header.setWorkshopName(detail.getWorkshopName());
  102. }
  103. header.setEntryCount(header.getEntryCount() + 1);
  104. if (detail.getAppQty() != null) {
  105. header.setTotalAppQty(header.getTotalAppQty().add(detail.getAppQty()));
  106. }
  107. if (detail.getActualQty() != null) {
  108. header.setTotalActualQty(header.getTotalActualQty().add(detail.getActualQty()));
  109. }
  110. }
  111. return new ArrayList<>(billMap.values());
  112. }
  113. /**
  114. * 生产订单下查:关联领料明细分录。
  115. */
  116. public List<ErpPickMaterialBill> findTraceListByMoBillNo(String moBillNo) {
  117. if (StringUtils.isBlank(moBillNo)) {
  118. return new ArrayList<>();
  119. }
  120. ErpPickMaterialBill query = new ErpPickMaterialBill();
  121. query.setMoBillNoQuery(moBillNo.trim());
  122. String moFilter = buildProductionOrderFilter(query);
  123. if (moFilter == null) {
  124. return new ArrayList<>();
  125. }
  126. return findDetailList(query, moFilter, 0, 2000);
  127. }
  128. /**
  129. * 明细页:指定领料单号的全部分录行。
  130. */
  131. public List<ErpPickMaterialBill> findDetailList(String billNo) {
  132. if (StringUtils.isBlank(billNo)) {
  133. return new ArrayList<>();
  134. }
  135. List<String> billNos = new ArrayList<>();
  136. billNos.add(billNo.trim());
  137. List<ErpPickMaterialBill> list = findDetailListByBillNos(billNos, null);
  138. try {
  139. mesErpPickKcService.increaseFromBillDetails(billNo.trim(), list);
  140. } catch (Exception ex) {
  141. logger.error("ERP pick bill detail MES kc sync failed, billNo={}", billNo, ex);
  142. }
  143. return list;
  144. }
  145. private List<ErpPickMaterialBill> findAggregatedPageList(ErpPickMaterialBill query, String moFilter,
  146. int startRow, int limit) {
  147. int targetSize = startRow + limit;
  148. int fetchStart = 0;
  149. int fetchLimit = Math.max(limit * 3, 50);
  150. int maxFetchRows = Math.max(targetSize * 5, 200);
  151. Map<String, ErpPickMaterialBill> billMap = new LinkedHashMap<>();
  152. while (billMap.size() < targetSize && fetchStart < maxFetchRows) {
  153. List<ErpPickMaterialBill> chunk = findDetailList(query, moFilter, fetchStart, fetchLimit);
  154. if (chunk.isEmpty()) {
  155. break;
  156. }
  157. for (ErpPickMaterialBill detail : chunk) {
  158. ErpPickMaterialBill header = billMap.get(detail.getBillNo());
  159. if (header == null) {
  160. header = toHeaderFromDetail(detail);
  161. header.setEntryCount(0);
  162. header.setTotalAppQty(BigDecimal.ZERO);
  163. header.setTotalActualQty(BigDecimal.ZERO);
  164. billMap.put(detail.getBillNo(), header);
  165. }
  166. header.setEntryCount(header.getEntryCount() + 1);
  167. if (detail.getAppQty() != null) {
  168. header.setTotalAppQty(header.getTotalAppQty().add(detail.getAppQty()));
  169. }
  170. if (detail.getActualQty() != null) {
  171. header.setTotalActualQty(header.getTotalActualQty().add(detail.getActualQty()));
  172. }
  173. }
  174. if (chunk.size() < fetchLimit) {
  175. break;
  176. }
  177. fetchStart += fetchLimit;
  178. }
  179. List<ErpPickMaterialBill> allBills = new ArrayList<>(billMap.values());
  180. if (startRow >= allBills.size()) {
  181. return new ArrayList<>();
  182. }
  183. return new ArrayList<>(allBills.subList(startRow, Math.min(startRow + limit, allBills.size())));
  184. }
  185. private List<ErpPickMaterialBill> findHeaderList(ErpPickMaterialBill query, String moFilter, int startRow, int limit) {
  186. JSONObject data = new JSONObject();
  187. data.put("FormId", FORM_ID);
  188. data.put("FieldKeys", HEADER_FIELD_KEYS);
  189. data.put("FilterString", buildFilterString(query, moFilter, false));
  190. data.put("OrderString", "FDate DESC,FBILLNO DESC");
  191. data.put("StartRow", Math.max(startRow, 0));
  192. data.put("Limit", Math.max(limit, 1));
  193. logger.info("ERP pick material bill header query: {}", data.toJSONString());
  194. return mapHeaderRows(erpClient.billQuery(data));
  195. }
  196. private List<ErpPickMaterialBill> findDetailList(ErpPickMaterialBill query, String moFilter, int startRow, int limit) {
  197. JSONObject data = new JSONObject();
  198. data.put("FormId", FORM_ID);
  199. data.put("FieldKeys", DETAIL_FIELD_KEYS);
  200. data.put("FilterString", buildFilterString(query, moFilter, true));
  201. data.put("OrderString", "FDate DESC,FBILLNO DESC,FEntity_FSeq");
  202. data.put("StartRow", Math.max(startRow, 0));
  203. data.put("Limit", Math.max(limit, 1));
  204. logger.info("ERP pick material bill detail query: {}", data.toJSONString());
  205. return mapDetailRows(erpClient.billQuery(data));
  206. }
  207. private List<ErpPickMaterialBill> findDetailListByBillNos(List<String> billNos, String moFilter) {
  208. if (billNos == null || billNos.isEmpty()) {
  209. return new ArrayList<>();
  210. }
  211. JSONObject data = new JSONObject();
  212. data.put("FormId", FORM_ID);
  213. data.put("FieldKeys", DETAIL_FIELD_KEYS);
  214. data.put("FilterString", buildInFilterString(billNos, moFilter));
  215. data.put("OrderString", "FBILLNO DESC,FEntity_FSeq");
  216. data.put("StartRow", 0);
  217. data.put("Limit", 2000);
  218. logger.info("ERP pick material bill detail by billNos query: {}", data.toJSONString());
  219. return mapDetailRows(erpClient.billQuery(data));
  220. }
  221. private List<ErpPickMaterialBill> mapHeaderRows(JSONArray rows) {
  222. Map<String, ErpPickMaterialBill> billMap = new LinkedHashMap<>();
  223. for (int i = 0; i < rows.size(); i++) {
  224. ErpPickMaterialBill bill = toHeader(rows.getJSONObject(i));
  225. if (StringUtils.isBlank(bill.getBillNo())) {
  226. continue;
  227. }
  228. billMap.putIfAbsent(bill.getBillNo(), bill);
  229. }
  230. return new ArrayList<>(billMap.values());
  231. }
  232. private List<ErpPickMaterialBill> mapDetailRows(JSONArray rows) {
  233. List<ErpPickMaterialBill> list = new ArrayList<>();
  234. for (int i = 0; i < rows.size(); i++) {
  235. ErpPickMaterialBill bill = toDetail(rows.getJSONObject(i), i);
  236. if (StringUtils.isBlank(bill.getMoBillNo())) {
  237. continue;
  238. }
  239. list.add(bill);
  240. }
  241. return list;
  242. }
  243. private ErpPickMaterialBill toHeader(JSONObject row) {
  244. ErpPickMaterialBill bill = new ErpPickMaterialBill();
  245. bill.setFid(readString(row, "FID"));
  246. bill.setBillNo(readString(row, "FBILLNO", "FBillNo"));
  247. bill.setId(bill.getBillNo());
  248. fillHeaderFields(bill, row);
  249. return bill;
  250. }
  251. private ErpPickMaterialBill toHeaderFromDetail(ErpPickMaterialBill detail) {
  252. ErpPickMaterialBill bill = new ErpPickMaterialBill();
  253. bill.setFid(detail.getFid());
  254. bill.setBillNo(detail.getBillNo());
  255. bill.setId(detail.getBillNo());
  256. bill.setDocumentStatus(detail.getDocumentStatus());
  257. bill.setDocumentStatusName(detail.getDocumentStatusName());
  258. bill.setDate(detail.getDate());
  259. bill.setPrdOrgNumber(detail.getPrdOrgNumber());
  260. bill.setPrdOrgName(detail.getPrdOrgName());
  261. bill.setStockOrgNumber(detail.getStockOrgNumber());
  262. bill.setStockOrgName(detail.getStockOrgName());
  263. bill.setMoBillNo(detail.getMoBillNo());
  264. bill.setWorkshopNumber(detail.getWorkshopNumber());
  265. bill.setWorkshopName(detail.getWorkshopName());
  266. return bill;
  267. }
  268. private ErpPickMaterialBill toDetail(JSONObject row, int index) {
  269. ErpPickMaterialBill bill = new ErpPickMaterialBill();
  270. bill.setFid(readString(row, "FID"));
  271. bill.setBillNo(readString(row, "FBILLNO", "FBillNo"));
  272. bill.setEntryId(readString(row, "FEntity_FEntryID", "FEntity_FEntryId"));
  273. bill.setSeq(firstNonBlank(
  274. readString(row, "FEntity_FSeq"),
  275. readString(row, "FMoEntrySeq")));
  276. String rowId = bill.getBillNo();
  277. if (StringUtils.isNotBlank(bill.getEntryId())) {
  278. rowId = rowId + "-" + bill.getEntryId();
  279. } else if (index >= 0) {
  280. rowId = rowId + "-" + index;
  281. }
  282. bill.setId(rowId);
  283. fillHeaderFields(bill, row);
  284. bill.setWorkshopNumber(readString(row,
  285. "FEntryWorkShopId.FNUMBER", "FEntryWorkShopId.FNumber",
  286. "FWorkShopId.FNUMBER", "FWorkShopId.FNumber"));
  287. bill.setWorkshopName(readString(row,
  288. "FEntryWorkShopId.FNAME", "FEntryWorkShopId.FName",
  289. "FWorkShopId.FNAME", "FWorkShopId.FName"));
  290. bill.setMaterialNumber(readString(row,
  291. "FMaterialId.FNUMBER", "FMaterialId.FNumber",
  292. "FEntity_FMaterialId.FNUMBER", "FEntity_FMaterialId.FNumber"));
  293. bill.setMaterialName(readString(row,
  294. "FMaterialName",
  295. "FMaterialId.FNAME", "FMaterialId.FName",
  296. "FEntity_FMaterialId.FNAME", "FEntity_FMaterialId.FName"));
  297. bill.setUnitName(readString(row,
  298. "FUnitID.FNAME", "FUnitId.FNAME", "FUnitId.FName",
  299. "FEntity_FUnitId.FNAME", "FEntity_FUnitId.FName"));
  300. bill.setAppQty(firstBigDecimal(row, "FAppQty", "FEntity_FAppQty"));
  301. bill.setActualQty(firstBigDecimal(row, "FActualQty", "FEntity_FActualQty"));
  302. bill.setMoBillNo(readString(row, MO_BILL_NO_FIELD, "FEntity_FMoBillNo"));
  303. bill.setPpbomBillNo(readString(row, "FPPBomBillNo", "FEntity_FPPBomBillNo"));
  304. bill.setStockName(readString(row,
  305. "FStockId.FNAME", "FStockId.FName",
  306. "FEntity_FStockId.FNAME", "FEntity_FStockId.FName"));
  307. return bill;
  308. }
  309. private void fillHeaderFields(ErpPickMaterialBill bill, JSONObject row) {
  310. bill.setDocumentStatus(readString(row, "FDocumentStatus"));
  311. bill.setDocumentStatusName(documentStatusName(bill.getDocumentStatus()));
  312. bill.setDate(formatErpDate(readString(row, "FDate")));
  313. bill.setPrdOrgNumber(readString(row, "FPrdOrgId.FNUMBER", "FPrdOrgId.FNumber"));
  314. bill.setPrdOrgName(readString(row, "FPrdOrgId.FNAME", "FPrdOrgId.FName"));
  315. bill.setStockOrgNumber(readString(row, "FStockOrgId.FNUMBER", "FStockOrgId.FNumber"));
  316. bill.setStockOrgName(readString(row, "FStockOrgId.FNAME", "FStockOrgId.FName"));
  317. }
  318. /**
  319. * 按 MES 可见生产订单范围筛选;无关联生产订单的领料行不查。
  320. *
  321. * @return null 表示当前条件下无需请求 ERP(结果为空)
  322. */
  323. private String buildProductionOrderFilter(ErpPickMaterialBill query) {
  324. String moBillNo = query == null ? "" : StringUtils.trim(query.getMoBillNoQuery());
  325. if (StringUtils.isNotBlank(moBillNo)) {
  326. if (!erpProductionOrderService.isVisibleProductionOrder(moBillNo)) {
  327. logger.info("ERP pick material bill skip: production order not in MES scope, moBillNo={}", moBillNo);
  328. return null;
  329. }
  330. return MO_BILL_NO_FIELD + "='" + escape(moBillNo) + "'";
  331. }
  332. List<String> visibleBillNos = erpProductionOrderService.findVisibleBillNos(VISIBLE_MO_LIMIT);
  333. if (visibleBillNos.isEmpty()) {
  334. logger.info("ERP pick material bill skip: no visible production orders");
  335. return null;
  336. }
  337. return buildInFilter(MO_BILL_NO_FIELD, visibleBillNos);
  338. }
  339. private String buildFilterString(ErpPickMaterialBill query, String moFilter, boolean includeMaterialFilter) {
  340. String filter = appendAnd(buildCurrentOrgFilter(), moFilter);
  341. // 排除未关联生产订单的分录
  342. filter = appendAnd(filter, MO_BILL_NO_FIELD + "<>''");
  343. if (query == null) {
  344. return filter;
  345. }
  346. if (StringUtils.isNotBlank(query.getBillNo())) {
  347. filter = appendAnd(filter, "FBILLNO='" + escape(query.getBillNo()) + "'");
  348. }
  349. if (includeMaterialFilter && StringUtils.isNotBlank(query.getMaterialNumberQuery())) {
  350. filter = appendAnd(filter,
  351. MATERIAL_NUMBER_FIELD + " like '%" + escape(query.getMaterialNumberQuery()) + "%'");
  352. }
  353. return filter;
  354. }
  355. private String buildInFilterString(List<String> billNos, String moFilter) {
  356. StringBuilder builder = new StringBuilder(appendAnd(buildCurrentOrgFilter(), MO_BILL_NO_FIELD + "<>''"));
  357. if (StringUtils.isNotBlank(moFilter)) {
  358. builder.append(" AND ").append(moFilter);
  359. }
  360. builder.append(" AND FBILLNO IN(");
  361. for (int i = 0; i < billNos.size(); i++) {
  362. if (i > 0) {
  363. builder.append(",");
  364. }
  365. builder.append("'").append(escape(billNos.get(i))).append("'");
  366. }
  367. builder.append(")");
  368. return builder.toString();
  369. }
  370. private String buildInFilter(String fieldName, List<String> values) {
  371. StringBuilder builder = new StringBuilder(fieldName).append(" IN(");
  372. for (int i = 0; i < values.size(); i++) {
  373. if (i > 0) {
  374. builder.append(",");
  375. }
  376. builder.append("'").append(escape(values.get(i))).append("'");
  377. }
  378. builder.append(")");
  379. return builder.toString();
  380. }
  381. private String buildCurrentOrgFilter() {
  382. if (!erpProperties.isFilterCurrentOrg()) {
  383. return "";
  384. }
  385. if (StringUtils.isNotBlank(erpProperties.getOrgNumber())) {
  386. return "FPrdOrgId.FNUMBER='" + escape(erpProperties.getOrgNumber()) + "'";
  387. }
  388. Long currentOrganizationId = erpClient.getCurrentOrganizationId();
  389. if (currentOrganizationId == null) {
  390. return "1=1";
  391. }
  392. return "FPrdOrgId=" + currentOrganizationId;
  393. }
  394. private String appendAnd(String left, String right) {
  395. if (StringUtils.isBlank(left)) {
  396. return right;
  397. }
  398. if (StringUtils.isBlank(right)) {
  399. return left;
  400. }
  401. return left + " AND " + right;
  402. }
  403. private String escape(String value) {
  404. return value == null ? "" : value.trim().replace("'", "''");
  405. }
  406. private String readString(JSONObject row, String... keys) {
  407. for (String key : keys) {
  408. String value = row.getString(key);
  409. if (StringUtils.isNotBlank(value)) {
  410. return value.trim();
  411. }
  412. }
  413. return "";
  414. }
  415. private String firstNonBlank(String... values) {
  416. for (String value : values) {
  417. if (StringUtils.isNotBlank(value)) {
  418. return value.trim();
  419. }
  420. }
  421. return "";
  422. }
  423. private BigDecimal firstBigDecimal(JSONObject row, String... keys) {
  424. for (String key : keys) {
  425. BigDecimal value = row.getBigDecimal(key);
  426. if (value != null) {
  427. return ErpKingdeeParseUtils.normalizeQtyNullable(value);
  428. }
  429. }
  430. return null;
  431. }
  432. private String formatErpDate(String rawValue) {
  433. if (StringUtils.isBlank(rawValue)) {
  434. return "";
  435. }
  436. String trimmed = rawValue.trim();
  437. return trimmed.length() >= 10 ? trimmed.substring(0, 10) : trimmed;
  438. }
  439. private String documentStatusName(String status) {
  440. if ("A".equals(status)) {
  441. return "创建";
  442. }
  443. if ("B".equals(status)) {
  444. return "审核中";
  445. }
  446. if ("C".equals(status)) {
  447. return "已审核";
  448. }
  449. if ("D".equals(status)) {
  450. return "重新审核";
  451. }
  452. return status;
  453. }
  454. }