|
|
@@ -0,0 +1,360 @@
|
|
|
+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<MesBatchSn> 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<MesBatchSn> loadAndValidateBatches(List<String> batchIds, String materialNumber) {
|
|
|
+ List<MesBatchSn> batches = mesBatchSnService.findByIds(batchIds);
|
|
|
+ List<MesBatchSn> 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<MesBatchSn> 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<ErpPrdInStock> 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<MesBatchSn> batches, ErpPrdInStockResult result,
|
|
|
+ String stockNumber, String stockLocNumber, BigDecimal totalQty, Date now) {
|
|
|
+ Set<String> 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();
|
|
|
+ }
|
|
|
+}
|