ErpPickMaterialBillService.java 15 KB

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