package com.jeesite.modules.mes.erp; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.jeesite.common.lang.StringUtils; import com.jeesite.modules.mes.entity.MesBatchSn; import com.jeesite.modules.mes.entity.MesErpPrdInstockLog; import com.jeesite.modules.mes.service.MesBatchSnService; import com.jeesite.modules.mes.service.MesErpPrdInstockLogService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * ERP 生产入库单 Save(PRD_INSTOCK)。 * 数量由 MES 批次码 mes_batch_sn.nums 汇总;ERP 批号 FLot 由 ERP 系统内部生成,Save 后回查。 */ @Service public class ErpPrdInStockSaveService { private static final Logger logger = LoggerFactory.getLogger(ErpPrdInStockSaveService.class); private static final String FORM_ID = "PRD_INSTOCK"; private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private final KingdeeErpClient erpClient; private final KingdeeErpProperties erpProperties; private final ErpProductionOrderService erpProductionOrderService; private final ErpProductionMaterialService erpProductionMaterialService; private final ErpPrdInStockService erpPrdInStockService; private final ErpStockLocService erpStockLocService; private final MesBatchSnService mesBatchSnService; private final MesErpPrdInstockLogService mesErpPrdInstockLogService; public ErpPrdInStockSaveService(KingdeeErpClient erpClient, KingdeeErpProperties erpProperties, ErpProductionOrderService erpProductionOrderService, ErpProductionMaterialService erpProductionMaterialService, ErpPrdInStockService erpPrdInStockService, ErpStockLocService erpStockLocService, MesBatchSnService mesBatchSnService, MesErpPrdInstockLogService mesErpPrdInstockLogService) { this.erpClient = erpClient; this.erpProperties = erpProperties; this.erpProductionOrderService = erpProductionOrderService; this.erpProductionMaterialService = erpProductionMaterialService; this.erpPrdInStockService = erpPrdInStockService; this.erpStockLocService = erpStockLocService; this.mesBatchSnService = mesBatchSnService; this.mesErpPrdInstockLogService = mesErpPrdInstockLogService; } @Transactional public ErpPrdInStockResult submit(ErpPrdInStockSubmitRequest request) { if (request == null || StringUtils.isBlank(request.getMoBillNo())) { return ErpPrdInStockResult.fail("生产订单号不能为空"); } if (StringUtils.isBlank(request.getEntryId())) { return ErpPrdInStockResult.fail("生产订单分录不能为空"); } if (request.getBatchIds() == null || request.getBatchIds().isEmpty()) { return ErpPrdInStockResult.fail("请至少选择一个 MES 批次码"); } ErpProductionOrder moEntry = erpProductionOrderService.findInstockMoEntry( request.getMoBillNo().trim(), request.getEntryId().trim()); if (moEntry == null) { return ErpPrdInStockResult.fail("未找到对应的生产订单分录"); } if (!erpProperties.isAllowedPrdInStockMaterial(moEntry.getMaterialNumber())) { return ErpPrdInStockResult.fail("该订单分录物料不在 HEV/115 入库范围内"); } List batches = loadAndValidateBatches(request.getBatchIds(), moEntry.getMaterialNumber()); if (batches.isEmpty()) { return ErpPrdInStockResult.fail("未找到有效的待入库批次码"); } BigDecimal totalQty = sumBatchQty(batches); if (totalQty.compareTo(BigDecimal.ZERO) <= 0) { return ErpPrdInStockResult.fail("所选批次码合计数量必须大于 0"); } String stockNumber = StringUtils.trim(request.getStockNumber()); if (StringUtils.isBlank(stockNumber)) { stockNumber = firstNonBlank(moEntry.getStockNumber(), erpProperties.getPrdInStockDefaultStockNumber(), erpProperties.getInStockStockNumber()); } if (StringUtils.isBlank(stockNumber)) { return ErpPrdInStockResult.fail("请选择入库仓库"); } String stockLocNumber = StringUtils.trim(request.getStockLocNumber()); if (erpStockLocService.requiresStockLocation(stockNumber) && StringUtils.isBlank(stockLocNumber)) { return ErpPrdInStockResult.fail("所选仓库需要选择仓位"); } String unitNumber = resolveUnitNumber(moEntry); if (StringUtils.isBlank(unitNumber)) { return ErpPrdInStockResult.fail("物料单位编码为空,无法入库"); } JSONObject saveData = buildSaveData(moEntry, totalQty, unitNumber, stockNumber, stockLocNumber); logger.info("ERP production in-stock save request: {}", saveData.toJSONString()); JSONObject response = erpClient.save(FORM_ID, saveData); ErpPrdInStockResult result = parseSaveResponse(response, totalQty, batches.size()); if (!result.isSuccess()) { return result; } String erpLotNumber = resolveErpLotNumber(result.getBillNo()); if (StringUtils.isNotBlank(erpLotNumber)) { result = ErpPrdInStockResult.success(result.getBillNo(), result.getBillId(), erpLotNumber, totalQty, batches.size()); } Date now = new Date(); mesBatchSnService.markInstocked(batches, moEntry.getBillNo(), moEntry.getEntryId(), result.getBillNo(), result.getLotNumber(), now); saveInstockLog(moEntry, batches, result, stockNumber, stockLocNumber, totalQty, now); return result; } /** 加载并校验 MES 批次码:nums>0、未入库、产线与物料匹配 */ private List loadAndValidateBatches(List batchIds, String materialNumber) { List batches = mesBatchSnService.findByIds(batchIds); List valid = new ArrayList<>(); for (MesBatchSn batch : batches) { if (batch == null) { continue; } if (batch.getNums() == null || batch.getNums() <= 0) { continue; } if ("1".equals(StringUtils.trim(batch.getInstockStatus()))) { continue; } if (!MesErpLineMaterialMapping.isLineAllowedForMaterial(batch.getLineSn(), materialNumber)) { continue; } valid.add(batch); } return valid; } private BigDecimal sumBatchQty(List batches) { BigDecimal total = BigDecimal.ZERO; for (MesBatchSn batch : batches) { if (batch.getNums() != null) { total = total.add(BigDecimal.valueOf(batch.getNums())); } } return ErpKingdeeParseUtils.normalizeQty(total); } private String resolveUnitNumber(ErpProductionOrder moEntry) { if (StringUtils.isNotBlank(moEntry.getUnitNumber())) { return moEntry.getUnitNumber().trim(); } return erpProductionMaterialService.findMaterialUnitNumber(moEntry.getMaterialNumber()); } private JSONObject buildSaveData(ErpProductionOrder moEntry, BigDecimal totalQty, String unitNumber, String stockNumber, String stockLocNumber) { String orgNumber = firstNonBlank(moEntry.getPrdOrgNumber(), erpProperties.getOrgNumber(), "110"); String ownerType = erpProperties.getPrdInStockOwnerType(); String keeperType = erpProperties.getPrdInStockKeeperType(); String stockStatus = erpProperties.getPrdInStockStockStatus(); Object erpQty = ErpKingdeeParseUtils.toErpQty(totalQty); JSONObject model = new JSONObject(true); model.put("FID", 0); model.put("FBillType", numberObject(erpProperties.getPrdInStockBillType(), true)); model.put("FDate", LocalDate.now().format(DATE_FORMAT)); model.put("FPrdOrgId", numberObject(orgNumber, false)); model.put("FStockOrgId", numberObject(orgNumber, false)); model.put("FOwnerTypeId0", ownerType); model.put("FOwnerId0", numberObject(orgNumber, false)); if (StringUtils.isNotBlank(moEntry.getWorkshopNumber())) { model.put("FWorkShopId", numberObject(moEntry.getWorkshopNumber(), false)); } JSONObject entity = new JSONObject(true); entity.put("FEntryID", 0); entity.put("FMaterialId", numberObject(moEntry.getMaterialNumber(), false)); entity.put("FInStockType", erpProperties.getPrdInStockInStockType()); entity.put("FUnitID", numberObject(unitNumber, false)); entity.put("FBaseUnitId", numberObject(unitNumber, false)); entity.put("FMustQty", erpQty); entity.put("FRealQty", erpQty); entity.put("FBaseMustQty", erpQty); entity.put("FBaseRealQty", erpQty); entity.put("FStockRealQty", erpQty); entity.put("FMoBillNo", moEntry.getBillNo()); entity.put("FMoId", parseLong(moEntry.getFid())); entity.put("FMoEntryId", parseLong(moEntry.getEntryId())); if (StringUtils.isNotBlank(moEntry.getSeq())) { entity.put("FMoEntrySeq", parseInt(moEntry.getSeq())); } entity.put("FOwnerTypeId", ownerType); entity.put("FOwnerId", numberObject(orgNumber, false)); entity.put("FKeeperTypeId", keeperType); entity.put("FKeeperId", numberObject(orgNumber, false)); entity.put("FStockId", numberObject(stockNumber, false)); entity.put("FStockStatusId", numberObject(stockStatus, false)); putStockLoc(entity, stockNumber, stockLocNumber); // 不传 FLot,由 ERP 系统内部生成批号 JSONArray entityList = new JSONArray(); entityList.add(entity); model.put("FEntity", entityList); JSONObject saveData = new JSONObject(true); saveData.put("NeedUpDateFields", new JSONArray()); saveData.put("NeedReturnFields", JSONArray.parseArray("[\"FBillNo\"]")); saveData.put("IsDeleteEntry", "true"); saveData.put("IsVerifyBaseDataField", "false"); saveData.put("IsEntryBatchFill", "true"); saveData.put("ValidateFlag", "true"); saveData.put("NumberSearch", "true"); saveData.put("IsAutoAdjustField", "true"); saveData.put("IgnoreInterationFlag", "true"); saveData.put("Model", model); return saveData; } private void putStockLoc(JSONObject entry, String stockNumber, String stockLocNumber) { if (!erpStockLocService.requiresStockLocation(stockNumber)) { return; } String locNumber = StringUtils.trim(stockLocNumber); if (StringUtils.isBlank(locNumber)) { return; } String locKey = erpStockLocService.resolveLocKeyForStock(stockNumber); String saveKey = erpProperties.toStockLocSaveKey(locKey); JSONObject flexValue = numberObject(locNumber, false); JSONObject stockLoc = new JSONObject(true); stockLoc.put(saveKey, flexValue); entry.put("FStockLocId", stockLoc); } /** Save 成功后 BillQuery 回查 ERP 内部批号 */ private String resolveErpLotNumber(String billNo) { if (StringUtils.isBlank(billNo)) { return ""; } List rows = erpPrdInStockService.findDetailList(billNo.trim()); for (ErpPrdInStock row : rows) { if (StringUtils.isNotBlank(row.getLotNumber())) { return row.getLotNumber().trim(); } } return ""; } private void saveInstockLog(ErpProductionOrder moEntry, List batches, ErpPrdInStockResult result, String stockNumber, String stockLocNumber, BigDecimal totalQty, Date now) { Set batchSnSet = new LinkedHashSet<>(); for (MesBatchSn batch : batches) { if (batch != null && StringUtils.isNotBlank(batch.getBatchSn())) { batchSnSet.add(batch.getBatchSn().trim()); } } MesErpPrdInstockLog log = new MesErpPrdInstockLog(); log.setIsNewRecord(true); log.setMoBillNo(moEntry.getBillNo()); log.setMoEntryId(moEntry.getEntryId()); log.setMaterialNumber(moEntry.getMaterialNumber()); log.setErpBillNo(result.getBillNo()); log.setErpLotNumber(result.getLotNumber()); log.setTotalQty(totalQty); log.setBatchCount(batches.size()); log.setStockNumber(stockNumber); log.setStockLocNumber(stockLocNumber); log.setBatchSnList(batchSnSet.stream().collect(Collectors.joining(","))); log.setStatus("0"); log.setCreateDate(now); log.setUpdateDate(now); mesErpPrdInstockLogService.saveLog(log); } private JSONObject numberObject(String number, boolean useFNumberKey) { JSONObject object = new JSONObject(); object.put(useFNumberKey ? "FNUMBER" : "FNumber", number); return object; } private long parseLong(String value) { if (StringUtils.isBlank(value)) { return 0L; } return Long.parseLong(value.trim()); } private int parseInt(String value) { if (StringUtils.isBlank(value)) { return 0; } return Integer.parseInt(value.trim()); } private String firstNonBlank(String... values) { for (String value : values) { if (StringUtils.isNotBlank(value)) { return value.trim(); } } return ""; } private ErpPrdInStockResult parseSaveResponse(JSONObject response, BigDecimal totalQty, int batchCount) { if (response == null) { return ErpPrdInStockResult.fail("ERP 返回为空"); } JSONObject resultWrapper = response.getJSONObject("Result"); JSONObject status = resultWrapper == null ? null : resultWrapper.getJSONObject("ResponseStatus"); if (status != null && Boolean.TRUE.equals(status.getBoolean("IsSuccess"))) { JSONObject result = resultWrapper.getJSONObject("Result"); String billNo = result == null ? "" : result.getString("Number"); if (StringUtils.isBlank(billNo)) { billNo = resultWrapper.getString("Number"); } String billId = result == null ? "" : result.getString("Id"); if (StringUtils.isBlank(billId)) { billId = resultWrapper.getString("Id"); } return ErpPrdInStockResult.success(billNo, billId, "", totalQty, batchCount); } return ErpPrdInStockResult.fail(extractErrorMessage(status, response)); } private String extractErrorMessage(JSONObject status, JSONObject response) { if (status != null) { JSONArray errors = status.getJSONArray("Errors"); if (errors != null && !errors.isEmpty()) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < errors.size(); i++) { JSONObject error = errors.getJSONObject(i); if (builder.length() > 0) { builder.append(";"); } builder.append(error.getString("Message")); } if (builder.length() > 0) { return builder.toString(); } } } return "ERP 生产入库单保存失败:" + response.toJSONString(); } }