ErpPrdInStockSaveService.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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 com.jeesite.modules.mes.entity.MesBatchSn;
  6. import com.jeesite.modules.mes.entity.MesErpPrdInstockLog;
  7. import com.jeesite.modules.mes.service.MesBatchSnService;
  8. import com.jeesite.modules.mes.service.MesErpPrdInstockLogService;
  9. import org.slf4j.Logger;
  10. import org.slf4j.LoggerFactory;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.transaction.annotation.Transactional;
  13. import java.math.BigDecimal;
  14. import java.time.LocalDate;
  15. import java.time.format.DateTimeFormatter;
  16. import java.util.ArrayList;
  17. import java.util.Date;
  18. import java.util.LinkedHashSet;
  19. import java.util.List;
  20. import java.util.Set;
  21. import java.util.stream.Collectors;
  22. /**
  23. * ERP 生产入库单 Save(PRD_INSTOCK)。
  24. * 数量由 MES 批次码 mes_batch_sn.nums 汇总;ERP 批号 FLot 由 ERP 系统内部生成,Save 后回查。
  25. */
  26. @Service
  27. public class ErpPrdInStockSaveService {
  28. private static final Logger logger = LoggerFactory.getLogger(ErpPrdInStockSaveService.class);
  29. private static final String FORM_ID = "PRD_INSTOCK";
  30. private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  31. private final KingdeeErpClient erpClient;
  32. private final KingdeeErpProperties erpProperties;
  33. private final ErpProductionOrderService erpProductionOrderService;
  34. private final ErpProductionMaterialService erpProductionMaterialService;
  35. private final ErpPrdInStockService erpPrdInStockService;
  36. private final ErpStockLocService erpStockLocService;
  37. private final MesBatchSnService mesBatchSnService;
  38. private final MesErpPrdInstockLogService mesErpPrdInstockLogService;
  39. public ErpPrdInStockSaveService(KingdeeErpClient erpClient,
  40. KingdeeErpProperties erpProperties,
  41. ErpProductionOrderService erpProductionOrderService,
  42. ErpProductionMaterialService erpProductionMaterialService,
  43. ErpPrdInStockService erpPrdInStockService,
  44. ErpStockLocService erpStockLocService,
  45. MesBatchSnService mesBatchSnService,
  46. MesErpPrdInstockLogService mesErpPrdInstockLogService) {
  47. this.erpClient = erpClient;
  48. this.erpProperties = erpProperties;
  49. this.erpProductionOrderService = erpProductionOrderService;
  50. this.erpProductionMaterialService = erpProductionMaterialService;
  51. this.erpPrdInStockService = erpPrdInStockService;
  52. this.erpStockLocService = erpStockLocService;
  53. this.mesBatchSnService = mesBatchSnService;
  54. this.mesErpPrdInstockLogService = mesErpPrdInstockLogService;
  55. }
  56. @Transactional
  57. public ErpPrdInStockResult submit(ErpPrdInStockSubmitRequest request) {
  58. if (request == null || StringUtils.isBlank(request.getMoBillNo())) {
  59. return ErpPrdInStockResult.fail("生产订单号不能为空");
  60. }
  61. if (StringUtils.isBlank(request.getEntryId())) {
  62. return ErpPrdInStockResult.fail("生产订单分录不能为空");
  63. }
  64. if (request.getBatchIds() == null || request.getBatchIds().isEmpty()) {
  65. return ErpPrdInStockResult.fail("请至少选择一个 MES 批次码");
  66. }
  67. ErpProductionOrder moEntry = erpProductionOrderService.findInstockMoEntry(
  68. request.getMoBillNo().trim(), request.getEntryId().trim());
  69. if (moEntry == null) {
  70. return ErpPrdInStockResult.fail("未找到对应的生产订单分录");
  71. }
  72. if (!erpProperties.isAllowedPrdInStockMaterial(moEntry.getMaterialNumber())) {
  73. return ErpPrdInStockResult.fail("该订单分录物料不在 HEV/115 入库范围内");
  74. }
  75. List<MesBatchSn> batches = loadAndValidateBatches(request.getBatchIds(), moEntry.getMaterialNumber());
  76. if (batches.isEmpty()) {
  77. return ErpPrdInStockResult.fail("未找到有效的待入库批次码");
  78. }
  79. BigDecimal totalQty = sumBatchQty(batches);
  80. if (totalQty.compareTo(BigDecimal.ZERO) <= 0) {
  81. return ErpPrdInStockResult.fail("所选批次码合计数量必须大于 0");
  82. }
  83. String stockNumber = StringUtils.trim(request.getStockNumber());
  84. if (StringUtils.isBlank(stockNumber)) {
  85. stockNumber = firstNonBlank(moEntry.getStockNumber(), erpProperties.getPrdInStockDefaultStockNumber(),
  86. erpProperties.getInStockStockNumber());
  87. }
  88. if (StringUtils.isBlank(stockNumber)) {
  89. return ErpPrdInStockResult.fail("请选择入库仓库");
  90. }
  91. String stockLocNumber = StringUtils.trim(request.getStockLocNumber());
  92. if (erpStockLocService.requiresStockLocation(stockNumber) && StringUtils.isBlank(stockLocNumber)) {
  93. return ErpPrdInStockResult.fail("所选仓库需要选择仓位");
  94. }
  95. String unitNumber = resolveUnitNumber(moEntry);
  96. if (StringUtils.isBlank(unitNumber)) {
  97. return ErpPrdInStockResult.fail("物料单位编码为空,无法入库");
  98. }
  99. JSONObject saveData = buildSaveData(moEntry, totalQty, unitNumber, stockNumber, stockLocNumber);
  100. logger.info("ERP production in-stock save request: {}", saveData.toJSONString());
  101. JSONObject response = erpClient.save(FORM_ID, saveData);
  102. ErpPrdInStockResult result = parseSaveResponse(response, totalQty, batches.size());
  103. if (!result.isSuccess()) {
  104. return result;
  105. }
  106. String erpLotNumber = resolveErpLotNumber(result.getBillNo());
  107. if (StringUtils.isNotBlank(erpLotNumber)) {
  108. result = ErpPrdInStockResult.success(result.getBillNo(), result.getBillId(), erpLotNumber,
  109. totalQty, batches.size());
  110. }
  111. Date now = new Date();
  112. mesBatchSnService.markInstocked(batches, moEntry.getBillNo(), moEntry.getEntryId(),
  113. result.getBillNo(), result.getLotNumber(), now);
  114. saveInstockLog(moEntry, batches, result, stockNumber, stockLocNumber, totalQty, now);
  115. return result;
  116. }
  117. /** 加载并校验 MES 批次码:nums>0、未入库、产线与物料匹配 */
  118. private List<MesBatchSn> loadAndValidateBatches(List<String> batchIds, String materialNumber) {
  119. List<MesBatchSn> batches = mesBatchSnService.findByIds(batchIds);
  120. List<MesBatchSn> valid = new ArrayList<>();
  121. for (MesBatchSn batch : batches) {
  122. if (batch == null) {
  123. continue;
  124. }
  125. if (batch.getNums() == null || batch.getNums() <= 0) {
  126. continue;
  127. }
  128. if ("1".equals(StringUtils.trim(batch.getInstockStatus()))) {
  129. continue;
  130. }
  131. if (!MesErpLineMaterialMapping.isLineAllowedForMaterial(batch.getLineSn(), materialNumber)) {
  132. continue;
  133. }
  134. valid.add(batch);
  135. }
  136. return valid;
  137. }
  138. private BigDecimal sumBatchQty(List<MesBatchSn> batches) {
  139. BigDecimal total = BigDecimal.ZERO;
  140. for (MesBatchSn batch : batches) {
  141. if (batch.getNums() != null) {
  142. total = total.add(BigDecimal.valueOf(batch.getNums()));
  143. }
  144. }
  145. return ErpKingdeeParseUtils.normalizeQty(total);
  146. }
  147. private String resolveUnitNumber(ErpProductionOrder moEntry) {
  148. if (StringUtils.isNotBlank(moEntry.getUnitNumber())) {
  149. return moEntry.getUnitNumber().trim();
  150. }
  151. return erpProductionMaterialService.findMaterialUnitNumber(moEntry.getMaterialNumber());
  152. }
  153. private JSONObject buildSaveData(ErpProductionOrder moEntry, BigDecimal totalQty, String unitNumber,
  154. String stockNumber, String stockLocNumber) {
  155. String orgNumber = firstNonBlank(moEntry.getPrdOrgNumber(), erpProperties.getOrgNumber(), "110");
  156. String ownerType = erpProperties.getPrdInStockOwnerType();
  157. String keeperType = erpProperties.getPrdInStockKeeperType();
  158. String stockStatus = erpProperties.getPrdInStockStockStatus();
  159. Object erpQty = ErpKingdeeParseUtils.toErpQty(totalQty);
  160. JSONObject model = new JSONObject(true);
  161. model.put("FID", 0);
  162. model.put("FBillType", numberObject(erpProperties.getPrdInStockBillType(), true));
  163. model.put("FDate", LocalDate.now().format(DATE_FORMAT));
  164. model.put("FPrdOrgId", numberObject(orgNumber, false));
  165. model.put("FStockOrgId", numberObject(orgNumber, false));
  166. model.put("FOwnerTypeId0", ownerType);
  167. model.put("FOwnerId0", numberObject(orgNumber, false));
  168. if (StringUtils.isNotBlank(moEntry.getWorkshopNumber())) {
  169. model.put("FWorkShopId", numberObject(moEntry.getWorkshopNumber(), false));
  170. }
  171. JSONObject entity = new JSONObject(true);
  172. entity.put("FEntryID", 0);
  173. entity.put("FMaterialId", numberObject(moEntry.getMaterialNumber(), false));
  174. entity.put("FInStockType", erpProperties.getPrdInStockInStockType());
  175. entity.put("FUnitID", numberObject(unitNumber, false));
  176. entity.put("FBaseUnitId", numberObject(unitNumber, false));
  177. entity.put("FMustQty", erpQty);
  178. entity.put("FRealQty", erpQty);
  179. entity.put("FBaseMustQty", erpQty);
  180. entity.put("FBaseRealQty", erpQty);
  181. entity.put("FStockRealQty", erpQty);
  182. entity.put("FMoBillNo", moEntry.getBillNo());
  183. entity.put("FMoId", parseLong(moEntry.getFid()));
  184. entity.put("FMoEntryId", parseLong(moEntry.getEntryId()));
  185. if (StringUtils.isNotBlank(moEntry.getSeq())) {
  186. entity.put("FMoEntrySeq", parseInt(moEntry.getSeq()));
  187. }
  188. entity.put("FOwnerTypeId", ownerType);
  189. entity.put("FOwnerId", numberObject(orgNumber, false));
  190. entity.put("FKeeperTypeId", keeperType);
  191. entity.put("FKeeperId", numberObject(orgNumber, false));
  192. entity.put("FStockId", numberObject(stockNumber, false));
  193. entity.put("FStockStatusId", numberObject(stockStatus, false));
  194. putStockLoc(entity, stockNumber, stockLocNumber);
  195. // 不传 FLot,由 ERP 系统内部生成批号
  196. JSONArray entityList = new JSONArray();
  197. entityList.add(entity);
  198. model.put("FEntity", entityList);
  199. JSONObject saveData = new JSONObject(true);
  200. saveData.put("NeedUpDateFields", new JSONArray());
  201. saveData.put("NeedReturnFields", JSONArray.parseArray("[\"FBillNo\"]"));
  202. saveData.put("IsDeleteEntry", "true");
  203. saveData.put("IsVerifyBaseDataField", "false");
  204. saveData.put("IsEntryBatchFill", "true");
  205. saveData.put("ValidateFlag", "true");
  206. saveData.put("NumberSearch", "true");
  207. saveData.put("IsAutoAdjustField", "true");
  208. saveData.put("IgnoreInterationFlag", "true");
  209. saveData.put("Model", model);
  210. return saveData;
  211. }
  212. private void putStockLoc(JSONObject entry, String stockNumber, String stockLocNumber) {
  213. if (!erpStockLocService.requiresStockLocation(stockNumber)) {
  214. return;
  215. }
  216. String locNumber = StringUtils.trim(stockLocNumber);
  217. if (StringUtils.isBlank(locNumber)) {
  218. return;
  219. }
  220. String locKey = erpStockLocService.resolveLocKeyForStock(stockNumber);
  221. String saveKey = erpProperties.toStockLocSaveKey(locKey);
  222. JSONObject flexValue = numberObject(locNumber, false);
  223. JSONObject stockLoc = new JSONObject(true);
  224. stockLoc.put(saveKey, flexValue);
  225. entry.put("FStockLocId", stockLoc);
  226. }
  227. /** Save 成功后 BillQuery 回查 ERP 内部批号 */
  228. private String resolveErpLotNumber(String billNo) {
  229. if (StringUtils.isBlank(billNo)) {
  230. return "";
  231. }
  232. List<ErpPrdInStock> rows = erpPrdInStockService.findDetailList(billNo.trim());
  233. for (ErpPrdInStock row : rows) {
  234. if (StringUtils.isNotBlank(row.getLotNumber())) {
  235. return row.getLotNumber().trim();
  236. }
  237. }
  238. return "";
  239. }
  240. private void saveInstockLog(ErpProductionOrder moEntry, List<MesBatchSn> batches, ErpPrdInStockResult result,
  241. String stockNumber, String stockLocNumber, BigDecimal totalQty, Date now) {
  242. Set<String> batchSnSet = new LinkedHashSet<>();
  243. for (MesBatchSn batch : batches) {
  244. if (batch != null && StringUtils.isNotBlank(batch.getBatchSn())) {
  245. batchSnSet.add(batch.getBatchSn().trim());
  246. }
  247. }
  248. MesErpPrdInstockLog log = new MesErpPrdInstockLog();
  249. log.setIsNewRecord(true);
  250. log.setMoBillNo(moEntry.getBillNo());
  251. log.setMoEntryId(moEntry.getEntryId());
  252. log.setMaterialNumber(moEntry.getMaterialNumber());
  253. log.setErpBillNo(result.getBillNo());
  254. log.setErpLotNumber(result.getLotNumber());
  255. log.setTotalQty(totalQty);
  256. log.setBatchCount(batches.size());
  257. log.setStockNumber(stockNumber);
  258. log.setStockLocNumber(stockLocNumber);
  259. log.setBatchSnList(batchSnSet.stream().collect(Collectors.joining(",")));
  260. log.setStatus("0");
  261. log.setCreateDate(now);
  262. log.setUpdateDate(now);
  263. mesErpPrdInstockLogService.saveLog(log);
  264. }
  265. private JSONObject numberObject(String number, boolean useFNumberKey) {
  266. JSONObject object = new JSONObject();
  267. object.put(useFNumberKey ? "FNUMBER" : "FNumber", number);
  268. return object;
  269. }
  270. private long parseLong(String value) {
  271. if (StringUtils.isBlank(value)) {
  272. return 0L;
  273. }
  274. return Long.parseLong(value.trim());
  275. }
  276. private int parseInt(String value) {
  277. if (StringUtils.isBlank(value)) {
  278. return 0;
  279. }
  280. return Integer.parseInt(value.trim());
  281. }
  282. private String firstNonBlank(String... values) {
  283. for (String value : values) {
  284. if (StringUtils.isNotBlank(value)) {
  285. return value.trim();
  286. }
  287. }
  288. return "";
  289. }
  290. private ErpPrdInStockResult parseSaveResponse(JSONObject response, BigDecimal totalQty, int batchCount) {
  291. if (response == null) {
  292. return ErpPrdInStockResult.fail("ERP 返回为空");
  293. }
  294. JSONObject resultWrapper = response.getJSONObject("Result");
  295. JSONObject status = resultWrapper == null ? null : resultWrapper.getJSONObject("ResponseStatus");
  296. if (status != null && Boolean.TRUE.equals(status.getBoolean("IsSuccess"))) {
  297. JSONObject result = resultWrapper.getJSONObject("Result");
  298. String billNo = result == null ? "" : result.getString("Number");
  299. if (StringUtils.isBlank(billNo)) {
  300. billNo = resultWrapper.getString("Number");
  301. }
  302. String billId = result == null ? "" : result.getString("Id");
  303. if (StringUtils.isBlank(billId)) {
  304. billId = resultWrapper.getString("Id");
  305. }
  306. return ErpPrdInStockResult.success(billNo, billId, "", totalQty, batchCount);
  307. }
  308. return ErpPrdInStockResult.fail(extractErrorMessage(status, response));
  309. }
  310. private String extractErrorMessage(JSONObject status, JSONObject response) {
  311. if (status != null) {
  312. JSONArray errors = status.getJSONArray("Errors");
  313. if (errors != null && !errors.isEmpty()) {
  314. StringBuilder builder = new StringBuilder();
  315. for (int i = 0; i < errors.size(); i++) {
  316. JSONObject error = errors.getJSONObject(i);
  317. if (builder.length() > 0) {
  318. builder.append(";");
  319. }
  320. builder.append(error.getString("Message"));
  321. }
  322. if (builder.length() > 0) {
  323. return builder.toString();
  324. }
  325. }
  326. }
  327. return "ERP 生产入库单保存失败:" + response.toJSONString();
  328. }
  329. }