diff --git a/mhd_wms/src/main/java/com/mhd/wms/application/service/stockReceiptOrder/StockReceiptOrderApplicationService.java b/mhd_wms/src/main/java/com/mhd/wms/application/service/stockReceiptOrder/StockReceiptOrderApplicationService.java index 9d314b6eb..452b33403 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/application/service/stockReceiptOrder/StockReceiptOrderApplicationService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/application/service/stockReceiptOrder/StockReceiptOrderApplicationService.java @@ -54,11 +54,14 @@ import org.springframework.util.CollectionUtils; import java.lang.reflect.Field; import java.math.BigDecimal; +import java.math.RoundingMode; import java.text.ParseException; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -296,8 +299,9 @@ public class StockReceiptOrderApplicationService { stockReceiptOrderPO.setTotalGrossWeight(totalGrossWeight); stockReceiptOrderPO.setTotalVolume(totalVolume); stockReceiptOrderPO.setTotalArea(totalArea); - // 扁平化明细:将子项收货数据合并到主项 - flattenMaterialDetailList(receiptMaterialDetailPOList); + // 扁平化明细:将子项收货数据合并到主项(按汇总已收推导状态,见方法内注释) + flattenMaterialDetailList(receiptMaterialDetailPOList, + stockReceiptOrderPO.getStatus() != null && stockReceiptOrderPO.getStatus() == 3); // PC 端子项计划数量不展示:递归清空所有子项的 quantity clearChildQuantityRecursively(receiptMaterialDetailPOList); // 计算待收货数量 = 计划数量 - 已收货数量 @@ -419,40 +423,159 @@ public class StockReceiptOrderApplicationService { } /** - * 扁平化明细:主行保留自身 receiptQuantity,alreadyReceiptQuantity 汇总主行+所有子项;保留父子结构,子项作为 children 展示 - * @param receiptMaterialDetailPOList 物料明细列表(树形,level 1 父级含 children) + * 扁平化明细:主行保留父子结构;展示「已收」= 主行累计 + 各子行累计(不含 parent_copy 镜像行,避免与主行重复)。 + * 件数、净重/毛重/体积/面积均用库表 already_receipt_* 口径(非 receipt_quantity)。 + *

+ * 子行除挂在 parent.children 外,若因树构建异常被提升为顶级或与父脱节,仍通过本单全表明细按 parent_unique_id 索引参与汇总,避免「只算一条已收」。 + * 已收件数/毛净重体面积与 {@link #sumAlreadyReceiptUnderPlanParent} / {@link #sumAlreadyMetricUnderPlanParent} 一致(与超计划校验、PDA 收货增量同一口径)。 + *

+ * 收货状态:须按「汇总已收 qtySum」与「主行计划量」比较推导,不能仅用库表主/子行 {@code receipt_status} 取大值; + * 否则主行+子行数量已收满时各表行仍可能为「收货中=2」,界面长期不显示已收货。 + * + * @param receiptOrderCompleted 收货单是否已整单完成(status=3);为 true 时少收为 4,与域服务完成口径一致 */ - private void flattenMaterialDetailList(List receiptMaterialDetailPOList) { + private void flattenMaterialDetailList(List receiptMaterialDetailPOList, + boolean receiptOrderCompleted) { if (CollectionUtils.isEmpty(receiptMaterialDetailPOList)) { return; } - for (ReceiptMaterialDetailPO parent : receiptMaterialDetailPOList) { - List children = parent.getChildren(); - if (CollectionUtils.isEmpty(children)) { + List allNodes = new ArrayList<>(); + collectReceiptMaterialDetailNodesRecursively(receiptMaterialDetailPOList, allNodes); + Map> childrenByParentUid = new HashMap<>(); + for (ReceiptMaterialDetailPO node : allNodes) { + if (node == null || node.getLevel() == null || node.getLevel() != 2) { continue; } - // 若存在 parent_copy(子项 uniqueId 与主项相同),主行数量仅取自子项,避免重复累加 - boolean hasParentCopy = children.stream().anyMatch(c -> parent.getUniqueId() != null && parent.getUniqueId().equals(c.getUniqueId())); - BigDecimal alreadyReceiptQuantitySum = (hasParentCopy ? BigDecimal.ZERO : (parent.getReceiptQuantity() != null ? parent.getReceiptQuantity() : BigDecimal.ZERO)); - Integer maxReceiptStatus = parent.getReceiptStatus(); - for (ReceiptMaterialDetailPO child : children) { - if (child.getReceiptQuantity() != null) { - alreadyReceiptQuantitySum = alreadyReceiptQuantitySum.add(child.getReceiptQuantity()); + Long puid = node.getParentUniqueId(); + if (puid == null || Long.valueOf(0).equals(puid)) { + continue; + } + childrenByParentUid.computeIfAbsent(puid, k -> new ArrayList<>()).add(node); + } + for (ReceiptMaterialDetailPO parent : receiptMaterialDetailPOList) { + if (parent == null) { + continue; + } + if (parent.getLevel() != null && parent.getLevel() != 1) { + continue; + } + // 全单范围内汇总「计划父行 + 其下 level=2」与收货/校验逻辑一致,避免仅依赖树上子集时漏掉主行首笔或脱节子行 + BigDecimal qtySum = sumAlreadyReceiptUnderPlanParent(parent, allNodes); + BigDecimal netSum = sumAlreadyMetricUnderPlanParent(parent, allNodes, ReceiptMaterialDetailPO::getAlreadyReceiptNetWeight); + BigDecimal grossSum = sumAlreadyMetricUnderPlanParent(parent, allNodes, ReceiptMaterialDetailPO::getAlreadyReceiptGrossWeight); + BigDecimal volSum = sumAlreadyMetricUnderPlanParent(parent, allNodes, ReceiptMaterialDetailPO::getAlreadyReceiptVolume); + BigDecimal areaSum = sumAlreadyMetricUnderPlanParent(parent, allNodes, ReceiptMaterialDetailPO::getAlreadyReceiptArea); + Integer derivedReceiptStatus = deriveReceiptStatusFromAggregatedReceivedVsPlan(qtySum, parent.getQuantity(), + receiptOrderCompleted); + + parent.setAlreadyReceiptQuantity(qtySum); + parent.setAlreadyReceiptNetWeight(netSum); + parent.setAlreadyReceiptGrossWeight(grossSum); + parent.setAlreadyReceiptVolume(volSum); + parent.setAlreadyReceiptArea(areaSum); + if (derivedReceiptStatus != null) { + parent.setReceiptStatus(derivedReceiptStatus); + } + + LinkedHashMap mergedChildren = new LinkedHashMap<>(); + mergeReceiptChildRowsIntoMap(mergedChildren, parent.getChildren()); + Long uid = parent.getUniqueId(); + Long inUid = parent.getInUniqueId(); + if (uid != null) { + mergeReceiptChildRowsIntoMap(mergedChildren, childrenByParentUid.get(uid)); + } + if (inUid != null && !inUid.equals(uid)) { + mergeReceiptChildRowsIntoMap(mergedChildren, childrenByParentUid.get(inUid)); + } + if (mergedChildren.isEmpty()) { + continue; + } + for (ReceiptMaterialDetailPO child : mergedChildren.values()) { + if (child == null) { + continue; } - if (child.getReceiptStatus() != null && (maxReceiptStatus == null || child.getReceiptStatus() > maxReceiptStatus)) { - maxReceiptStatus = child.getReceiptStatus(); + // parent_copy:与主行同一条库记录镜像,不累加(否则与主行重复) + if (parent.getUniqueId() != null && parent.getUniqueId().equals(child.getUniqueId())) { + child.setQuantity(null); + continue; } child.setQuantity(null); + // 与主行同卡片的真实子行:回显与主行一致(避免库表仍停留在收货中=2) + if (derivedReceiptStatus != null && child.getLevel() != null && child.getLevel() == 2) { + child.setReceiptStatus(derivedReceiptStatus); + } } - parent.setAlreadyReceiptQuantity(alreadyReceiptQuantitySum); - // 累计已称毛重不按子行汇总到主行:多批/多子行各自一条计划毛重,汇总会大于主行计划,导致 计划−已称=0 且界面显示 0E-8;待收毛重按本行 already_receipt_gross_weight 与 total_gross_weight 计算即可 - if (maxReceiptStatus != null) { - parent.setReceiptStatus(maxReceiptStatus); - } - // 保留父子结构,子项在 parent.children 中,前端按树形展示 } } + /** + * 主行展示用收货状态:按「主行+子行汇总已收」与主行计划量比较。 + * 与 {@link com.mhd.wms.domain.stockReceiptOrder.service.StockReceiptOrderDomainService} 中分批/完成收货的 1/2/3/4/5 含义一致。 + */ + private static Integer deriveReceiptStatusFromAggregatedReceivedVsPlan(BigDecimal aggregatedReceived, BigDecimal planQty, + boolean receiptOrderCompleted) { + BigDecimal r = aggregatedReceived != null ? aggregatedReceived : BigDecimal.ZERO; + BigDecimal p = planQty != null ? planQty : BigDecimal.ZERO; + if (receiptOrderCompleted) { + int cmp = r.compareTo(p); + if (cmp > 0) { + return 5; + } + if (cmp < 0) { + return 4; + } + return 3; + } + if (r.compareTo(BigDecimal.ZERO) == 0) { + return 1; + } + int cmp = r.compareTo(p); + if (cmp < 0) { + return 2; + } + if (cmp > 0) { + return 5; + } + return 3; + } + + private static void collectReceiptMaterialDetailNodesRecursively(List list, List out) { + if (CollectionUtils.isEmpty(list)) { + return; + } + for (ReceiptMaterialDetailPO n : list) { + if (n == null) { + continue; + } + out.add(n); + collectReceiptMaterialDetailNodesRecursively(n.getChildren(), out); + } + } + + /** 按 unique_id(缺省时 material_detail_id)去重合并子行,避免树内与索引重复计数 */ + private static void mergeReceiptChildRowsIntoMap(Map byKey, List children) { + if (CollectionUtils.isEmpty(children)) { + return; + } + for (ReceiptMaterialDetailPO c : children) { + if (c == null) { + continue; + } + Long key = c.getUniqueId(); + if (key == null) { + key = c.getMaterialDetailId(); + } + if (key == null) { + continue; + } + byKey.putIfAbsent(key, c); + } + } + + private static BigDecimal nzBig(BigDecimal v) { + return v != null ? v : BigDecimal.ZERO; + } + /** * PC 端子项计划数量不展示:递归清空所有子项的 quantity */ @@ -594,9 +717,16 @@ public class StockReceiptOrderApplicationService { } /** - * 计划/待收:plan* 来自入库单联表(与首次创建时 total 同源);pending* = plan − 累计已收,仅接口计算不落库。 + * 计划/待收:plan* 来自入库单联表;pending* = plan* − 累计已收(展示即「计划 − 已收」)。 + * 主行 already* 须在 {@link #flattenMaterialDetailList} 后表示该卡「全部已收」。 + * level=2 子行仍保留单行库表 already*,但 pending* 与主行同口径:使用传入的 {@code planParent}(对应 level=1 主行)上已汇总的已收,避免「只减子行本批」导致待收偏大。 + * 仅接口计算不落库。 */ private void fillPlanAndPendingMetricsRecursively(List list) { + fillPlanAndPendingMetricsRecursively(list, null); + } + + private void fillPlanAndPendingMetricsRecursively(List list, ReceiptMaterialDetailPO planParent) { if (CollectionUtils.isEmpty(list)) { return; } @@ -605,12 +735,26 @@ public class StockReceiptOrderApplicationService { continue; } if (!isPdaReceiptDetailFullyReceived(d)) { - d.setPendingNetWeight(calcPendingMetric(d.getPlanNetWeight(), d.getAlreadyReceiptNetWeight())); - d.setPendingGrossWeight(calcPendingMetric(d.getPlanGrossWeight(), d.getAlreadyReceiptGrossWeight())); - d.setPendingVolume(calcPendingMetric(d.getPlanVolume(), d.getAlreadyReceiptVolume())); - d.setPendingArea(calcPendingMetric(d.getPlanArea(), d.getAlreadyReceiptArea())); + BigDecimal netAlready = d.getAlreadyReceiptNetWeight(); + BigDecimal grossAlready = d.getAlreadyReceiptGrossWeight(); + BigDecimal volAlready = d.getAlreadyReceiptVolume(); + BigDecimal areaAlready = d.getAlreadyReceiptArea(); + if (planParent != null && d.getLevel() != null && d.getLevel() == 2) { + netAlready = planParent.getAlreadyReceiptNetWeight(); + grossAlready = planParent.getAlreadyReceiptGrossWeight(); + volAlready = planParent.getAlreadyReceiptVolume(); + areaAlready = planParent.getAlreadyReceiptArea(); + } + d.setPendingNetWeight(calcPendingMetric(d.getPlanNetWeight(), netAlready)); + d.setPendingGrossWeight(calcPendingMetric(d.getPlanGrossWeight(), grossAlready)); + d.setPendingVolume(calcPendingMetric(d.getPlanVolume(), volAlready)); + d.setPendingArea(calcPendingMetric(d.getPlanArea(), areaAlready)); } - fillPlanAndPendingMetricsRecursively(d.getChildren()); + ReceiptMaterialDetailPO nextPlanParent = planParent; + if (d.getLevel() != null && d.getLevel() == 1) { + nextPlanParent = d; + } + fillPlanAndPendingMetricsRecursively(d.getChildren(), nextPlanParent); } } @@ -1133,7 +1277,13 @@ public class StockReceiptOrderApplicationService { /** * 获取收货单详细信息(PDA 端) - * 在 Domain 层结果基础上,补充包装规格填充,与 PC 端 getInfo 一致 + * 在 Domain 层结果基础上,补充包装规格填充,与 PC 端 getInfo 一致。 + *

+ * 净重/毛重/体积/面积展示口径(待收 pending*;未完成明细覆盖后的 total* 与之一致):
+ * 待收 = 计划(plan*) − 累计已收(already_receipt_*)
+ * 对 level=1 主行(一张卡)already_receipt_* 在 {@link #flattenMaterialDetailList} 中已汇总为「主行本行 + 其下全部真实子行」的累计已收, + * 即「计划 − 全部已收」。子行单行库表 already_receipt_* 仍保留(便于看本批已收);子行 pending*(及后续覆盖的 total*) + * 与主行同口径:plan* − 主行上该卡累计已收,避免仅按子行本行已收算出偏大的待收。 */ public StockReceiptOrderPO getInfoByApp(Long receiptOrderId, Integer receiptStatus) { @@ -1191,8 +1341,9 @@ public class StockReceiptOrderApplicationService { fillBarCodeFromMaterialBarCodeRecursively(materialDetailList, materialBarCodeMap); // 第一次收货:主项无子项时,收货数量默认等于计划数量 fillFirstReceiptQuantityDefault(materialDetailList); - // PDA 端与 PC 端一致:主行收货数量取第一条子项,已收货数量汇总所有子项 - flattenMaterialDetailList(materialDetailList); + // 主行:件数/毛净重体面积已收 = 主行库表 + 其下子行汇总,用于「计划 − 全部已收」 + flattenMaterialDetailList(materialDetailList, + stockReceiptOrderPO.getStatus() != null && stockReceiptOrderPO.getStatus() == 3); // 须先算待收指标再清空子项 quantity;已收尾明细(3/4/5)不按「计划−待收」计算,见 isPdaReceiptDetailFullyReceived if (receiptStatus != null && receiptStatus == 1) { fillPendingDisplayFieldsRecursively(materialDetailList); @@ -1201,16 +1352,58 @@ public class StockReceiptOrderApplicationService { fillPendingReceiptQuantityRecursively(materialDetailList); syncReceiptQuantityCumulativeDisplayRecursively(materialDetailList); } + // pending* = plan* − already_receipt_*(主行 already* 已为全部已收汇总) fillPlanAndPendingMetricsRecursively(materialDetailList); - // PDA 绑定 total* 展示「待收」= plan − 已收;已收货行不覆盖 + // 未完成行:total* 与 pending* 同值,即「计划总的 − 全部已收」 overwriteTotalWithPendingForPdaDisplayRecursively(materialDetailList); clearChildQuantityRecursively(materialDetailList); + // 仅在未按 tab 筛明细时纠正整单头状态;筛成「待收/已收」子集时不能据子集推断整单是否已收完 + if (receiptStatus == null) { + refreshPdaReceiptOrderHeaderStatusFromDerivedPlanLines(stockReceiptOrderPO, materialDetailList); + } } return stockReceiptOrderPO; } /** - * PDA 界面「总净重/总毛重/体积/面积」绑定 total*:有计划时展示待收(pending*);无计划联表时保留原 total*(累计实绩)。 + * PDA 详情:header.status 直接来自库表;若库表仍为部分收货(2),但 flatten 后各计划主行已是收尾明细状态且整单收货数已不少于计划,则将返回头状态纠正为已收货(3),避免列表/详情长期显示「部分收货」。 + */ + private void refreshPdaReceiptOrderHeaderStatusFromDerivedPlanLines(StockReceiptOrderPO order, + List planRoots) { + if (order == null || order.getStatus() == null || order.getStatus() != 2) { + return; + } + if (CollectionUtils.isEmpty(planRoots)) { + return; + } + for (ReceiptMaterialDetailPO p : planRoots) { + if (p == null) { + continue; + } + if (p.getLevel() != null && p.getLevel() != 1) { + continue; + } + if (!isPdaTerminalReceiptDetailStatus(p.getReceiptStatus())) { + return; + } + } + BigDecimal rq = order.getReceiptQuantity() != null ? order.getReceiptQuantity() : BigDecimal.ZERO; + BigDecimal pq = order.getQuantity() != null ? order.getQuantity() : BigDecimal.ZERO; + if (rq.compareTo(pq) >= 0) { + order.setStatus(3); + } + } + + /** + * PDA:指定计划行(卡片)下分批收货子行列表(level=2),域服务已挂序列号。 + */ + public List listLevel2ChildrenByParentForApp(Long receiptOrderId, Long parentMaterialDetailId) { + return stockReceiptOrderDomainService.listLevel2ChildrenByParentForApp(receiptOrderId, parentMaterialDetailId); + } + + /** + * PDA 详情:将 total* 覆盖为待收(与 pending* 同值),便于前端仍绑 total* 时展示「计划 − 主项+子项全部已收」。 + * 回传收货时由 {@link #pickThisReceiptMetricIncrementPda} 识别并按件数比例分摊增量。 */ private void overwriteTotalWithPendingForPdaDisplayRecursively(List list) { if (CollectionUtils.isEmpty(list)) { @@ -1391,8 +1584,16 @@ public class StockReceiptOrderApplicationService { */ public Boolean receivingGoodsByApp(StockReceiptOrderDO stockReceiptOrderDO){ List list = stockReceiptOrderDO.getMaterialDetailList(); - // 超计划提示:实际收货数量 > 待收数量(计划-已收) 时,先提示确认 - checkOverPlanReceiptForApp(stockReceiptOrderDO); + StockReceiptOrderPO dbOrder = stockReceiptOrderDomainService.getInfo(stockReceiptOrderDO.getReceiptOrderId()); + if (dbOrder == null) { + throw new ServiceException("收货单不存在"); + } + ReceiptMaterialDetailDO queryAll = new ReceiptMaterialDetailDO(); + queryAll.setReceiptOrderNumber(dbOrder.getReceiptOrderNumber()); + List dbListAll = receiptMaterialDetailDomainService.queryList(queryAll); + preserveParentReceiptLineHierarchyFromDb(stockReceiptOrderDO, dbListAll); + // 超计划:按「计划父行 + 其下 level=2 子行」累计已收 算待收 + checkOverPlanReceiptForApp(stockReceiptOrderDO, dbListAll); // 记录 PDA 传入时第一项是否已有 children boolean firstItemHadChildrenBefore = !CollectionUtils.isEmpty(list) && !CollectionUtils.isEmpty(list.get(0).getChildren()); @@ -1404,6 +1605,30 @@ public class StockReceiptOrderApplicationService { // 主项子项结构:将 children 第一条(parent_copy)赋值给主项并删除 assignFirstChildToParentAndRemove(stockReceiptOrderDO); } + // assign 后若仍有真实子行,不再 inject(避免与已有子项结构冲突) + boolean skipInjectAfterAssign = firstItemHadChildrenBefore + && !CollectionUtils.isEmpty(stockReceiptOrderDO.getMaterialDetailList()) + && !CollectionUtils.isEmpty(stockReceiptOrderDO.getMaterialDetailList().get(0).getChildren()); + Map dbByIdForInject = dbListAll.stream() + .filter(x -> x.getMaterialDetailId() != null) + .collect(Collectors.toMap(ReceiptMaterialDetailPO::getMaterialDetailId, Function.identity(), (a, b) -> a)); + Map dbByUidForInject = dbListAll.stream() + .filter(x -> x.getUniqueId() != null) + .collect(Collectors.toMap(ReceiptMaterialDetailPO::getUniqueId, Function.identity(), (a, b) -> a)); + int injectIdx = 0; + for (ReceiptMaterialDetailDO line : stockReceiptOrderDO.getMaterialDetailList()) { + ReceiptMaterialDetailPO dbLine = null; + if (line.getMaterialDetailId() != null) { + dbLine = dbByIdForInject.get(line.getMaterialDetailId()); + } + if (dbLine == null && line.getUniqueId() != null) { + dbLine = dbByUidForInject.get(line.getUniqueId()); + } + if (dbLine != null && !(skipInjectAfterAssign && injectIdx == 0)) { + injectPdaFlatReceiptAsSyntheticChild(line, dbLine, dbListAll); + } + injectIdx++; + } StockReceiptOrderDO assembleStockReceiptOrderDO = assembleParamByReceipt(stockReceiptOrderDO, false); if (CollectionUtils.isEmpty(assembleStockReceiptOrderDO.getMaterialDetailList())) { throw new ServiceException("无有效收货数据,请确认已填写收货数量(收货数量不能全部为0)"); @@ -1412,17 +1637,13 @@ public class StockReceiptOrderApplicationService { return stockReceiptOrderDomainService.receivingGoods(assembleStockReceiptOrderDO); } - private void checkOverPlanReceiptForApp(StockReceiptOrderDO stockReceiptOrderDO) { + private void checkOverPlanReceiptForApp(StockReceiptOrderDO stockReceiptOrderDO, List dbList) { if (stockReceiptOrderDO == null || CollectionUtils.isEmpty(stockReceiptOrderDO.getMaterialDetailList())) { return; } - StockReceiptOrderPO dbOrder = stockReceiptOrderDomainService.getInfo(stockReceiptOrderDO.getReceiptOrderId()); - if (dbOrder == null) { + if (CollectionUtils.isEmpty(dbList)) { return; } - ReceiptMaterialDetailDO queryDO = new ReceiptMaterialDetailDO(); - queryDO.setReceiptOrderNumber(dbOrder.getReceiptOrderNumber()); - List dbList = receiptMaterialDetailDomainService.queryList(queryDO); Map dbById = dbList.stream() .filter(x -> x.getMaterialDetailId() != null) .collect(Collectors.toMap(ReceiptMaterialDetailPO::getMaterialDetailId, Function.identity(), (a, b) -> a)); @@ -1430,13 +1651,17 @@ public class StockReceiptOrderApplicationService { .filter(x -> x.getUniqueId() != null) .collect(Collectors.toMap(ReceiptMaterialDetailPO::getUniqueId, Function.identity(), (a, b) -> a)); for (ReceiptMaterialDetailDO item : stockReceiptOrderDO.getMaterialDetailList()) { - validateOverPlanNode(item, dbById, dbByUniqueId); + validateOverPlanNode(item, dbById, dbByUniqueId, dbList); } } + /** + * 分批收货:计划数量在 level=1 父行;待收 = 计划父行 quantity −(父行+其下 level=2 子行 already_receipt 汇总) + */ private void validateOverPlanNode(ReceiptMaterialDetailDO item, Map dbById, - Map dbByUniqueId) { + Map dbByUniqueId, + List dbList) { if (item == null) { return; } @@ -1448,9 +1673,10 @@ public class StockReceiptOrderApplicationService { db = dbByUniqueId.get(item.getUniqueId()); } if (db != null) { - BigDecimal plan = db.getQuantity() != null ? db.getQuantity() : BigDecimal.ZERO; - BigDecimal already = db.getAlreadyReceiptQuantity() != null ? db.getAlreadyReceiptQuantity() : BigDecimal.ZERO; - BigDecimal pending = plan.subtract(already); + ReceiptMaterialDetailPO plan = resolveReceiptPlanParentRow(db, dbList); + BigDecimal planQty = plan != null && plan.getQuantity() != null ? plan.getQuantity() : BigDecimal.ZERO; + BigDecimal sumAlready = sumAlreadyReceiptUnderPlanParent(plan, dbList); + BigDecimal pending = planQty.subtract(sumAlready); if (pending.compareTo(BigDecimal.ZERO) < 0) { pending = BigDecimal.ZERO; } @@ -1461,11 +1687,321 @@ public class StockReceiptOrderApplicationService { } if (!CollectionUtils.isEmpty(item.getChildren())) { for (ReceiptMaterialDetailDO child : item.getChildren()) { - validateOverPlanNode(child, dbById, dbByUniqueId); + validateOverPlanNode(child, dbById, dbByUniqueId, dbList); } } } + /** 雪花/长整型 id 相较,避免部分环境下引用类型不一致 */ + private static boolean sameLongId(Long a, Long b) { + if (a == null || b == null) { + return false; + } + return a.longValue() == b.longValue(); + } + + /** + * 是否为本计划父行下的 level=2 实子行(可参与已收汇总)。 + * 排除 parent_unique_id 空/0;排除与主行同 unique_id 的镜像行(同源只计主行对象一次)。 + */ + private static boolean isLevel2ChildRowUnderPlanParent(ReceiptMaterialDetailPO planParent, ReceiptMaterialDetailPO x) { + if (planParent == null || x == null) { + return false; + } + if (x.getLevel() == null || x.getLevel() != 2) { + return false; + } + Long puid = x.getParentUniqueId(); + if (puid == null || Long.valueOf(0).equals(puid)) { + return false; + } + Long uid = planParent.getUniqueId(); + Long inUid = planParent.getInUniqueId(); + boolean linked = (uid != null && sameLongId(uid, puid)) || (inUid != null && sameLongId(inUid, puid)); + if (!linked) { + return false; + } + if (planParent.getUniqueId() != null && sameLongId(planParent.getUniqueId(), x.getUniqueId())) { + return false; + } + return true; + } + + /** 计划父行:level=1 自身;level=2 则按 parent_unique_id 找到主计划行 */ + private static ReceiptMaterialDetailPO resolveReceiptPlanParentRow(ReceiptMaterialDetailPO db, + List dbList) { + if (db == null || CollectionUtils.isEmpty(dbList)) { + return db; + } + if (db.getLevel() == null || db.getLevel() == 1) { + return db; + } + if (db.getLevel() == 2 && db.getParentUniqueId() != null) { + Long puid = db.getParentUniqueId(); + for (ReceiptMaterialDetailPO x : dbList) { + if (x == null) { + continue; + } + if (puid.equals(x.getUniqueId()) || puid.equals(x.getInUniqueId())) { + return x; + } + } + } + return db; + } + + /** + * 父行自身 + 其下 level=2(parent 指向父)的已收数量之和。 + *

主行本行必须取传入的 {@code planParent} 上的累计字段,再在列表中只加子行;勿依赖在 {@code dbList} 中再次按 material_detail_id + * 匹配主行,否则 JDBC 类型/映射不一致时会出现「只加子行、主行 100 丢失」。

+ */ + private static BigDecimal sumAlreadyReceiptUnderPlanParent(ReceiptMaterialDetailPO planParent, + List dbList) { + if (planParent == null || CollectionUtils.isEmpty(dbList)) { + return BigDecimal.ZERO; + } + BigDecimal sum = planParent.getAlreadyReceiptQuantity() != null + ? planParent.getAlreadyReceiptQuantity() : BigDecimal.ZERO; + for (ReceiptMaterialDetailPO x : dbList) { + if (x == null || !isLevel2ChildRowUnderPlanParent(planParent, x)) { + continue; + } + BigDecimal ar = x.getAlreadyReceiptQuantity() != null ? x.getAlreadyReceiptQuantity() : BigDecimal.ZERO; + sum = sum.add(ar); + } + return sum; + } + + /** 与 {@link #sumAlreadyReceiptUnderPlanParent} 同范围,汇总任意「已收」指标(毛/净重、体积、面积等) */ + private static BigDecimal sumAlreadyMetricUnderPlanParent(ReceiptMaterialDetailPO planParent, + List dbList, + Function alreadyGetter) { + if (planParent == null || CollectionUtils.isEmpty(dbList) || alreadyGetter == null) { + return BigDecimal.ZERO; + } + BigDecimal base = alreadyGetter.apply(planParent); + BigDecimal sum = base != null ? base : BigDecimal.ZERO; + for (ReceiptMaterialDetailPO x : dbList) { + if (x == null || !isLevel2ChildRowUnderPlanParent(planParent, x)) { + continue; + } + BigDecimal v = alreadyGetter.apply(x); + sum = sum.add(v != null ? v : BigDecimal.ZERO); + } + return sum; + } + + /** 首笔不拆子行;已有子行或主行+子行已有累计实收时再拆 */ + private static boolean shouldPdaSplitIntoChildRow(ReceiptMaterialDetailPO dbLine, + List allDbLines) { + if (dbLine == null || CollectionUtils.isEmpty(allDbLines)) { + return true; + } + ReceiptMaterialDetailPO plan = resolveReceiptPlanParentRow(dbLine, allDbLines); + if (plan == null) { + return true; + } + Long uid = plan.getUniqueId(); + Long inUid = plan.getInUniqueId(); + boolean hasL2 = allDbLines.stream().anyMatch(x -> x != null && x.getLevel() != null && x.getLevel() == 2 + && x.getParentUniqueId() != null + && ((uid != null && uid.equals(x.getParentUniqueId())) + || (inUid != null && inUid.equals(x.getParentUniqueId())))); + if (hasL2) { + return true; + } + BigDecimal sum = sumAlreadyReceiptUnderPlanParent(plan, allDbLines); + return sum.compareTo(BigDecimal.ZERO) > 0; + } + + private static boolean isCorruptedStandaloneLineAsLevel2(ReceiptMaterialDetailPO dbLine) { + if (dbLine == null || dbLine.getLevel() == null || dbLine.getLevel() != 2) { + return false; + } + return dbLine.getParentUniqueId() == null || Objects.equals(dbLine.getParentUniqueId(), 0L); + } + + /** 合成子行挂在计划父行的 uniqueId;误标为 level=2 且无父时,按同物料 level=1 行定位 */ + private static Long resolveParentUniqueIdForPdaInjectChild(ReceiptMaterialDetailPO dbLine, + List allDbLines) { + if (dbLine == null) { + return null; + } + if (isCorruptedStandaloneLineAsLevel2(dbLine) && dbLine.getMaterialDetailId() != null + && !CollectionUtils.isEmpty(allDbLines)) { + for (ReceiptMaterialDetailPO x : allDbLines) { + if (x != null && Objects.equals(x.getMaterialDetailId(), dbLine.getMaterialDetailId()) + && x.getLevel() != null && x.getLevel() == 1) { + return x.getUniqueId(); + } + } + } + ReceiptMaterialDetailPO plan = resolveReceiptPlanParentRow(dbLine, allDbLines); + return plan != null ? plan.getUniqueId() : dbLine.getUniqueId(); + } + + /** 请求行与库主计划行 level 不一致时纠正,避免误当子行 */ + private void preserveParentReceiptLineHierarchyFromDb(StockReceiptOrderDO order, List dbList) { + if (order == null || CollectionUtils.isEmpty(order.getMaterialDetailList()) || CollectionUtils.isEmpty(dbList)) { + return; + } + Map byId = dbList.stream() + .filter(x -> x.getMaterialDetailId() != null) + .collect(Collectors.toMap(ReceiptMaterialDetailPO::getMaterialDetailId, Function.identity(), (a, b) -> a)); + Map byUid = dbList.stream() + .filter(x -> x.getUniqueId() != null) + .collect(Collectors.toMap(ReceiptMaterialDetailPO::getUniqueId, Function.identity(), (a, b) -> a)); + for (ReceiptMaterialDetailDO line : order.getMaterialDetailList()) { + ReceiptMaterialDetailPO db = null; + if (line.getMaterialDetailId() != null) { + db = byId.get(line.getMaterialDetailId()); + } + if (db == null && line.getUniqueId() != null) { + db = byUid.get(line.getUniqueId()); + } + if (db != null && db.getLevel() != null && db.getLevel() == 1 + && line.getLevel() != null && line.getLevel() == 2) { + line.setLevel(1); + line.setParentUniqueId(null); + } + } + } + + private static void stripPdaUiParentCopyChildren(ReceiptMaterialDetailDO line, ReceiptMaterialDetailPO dbLine) { + if (line == null || CollectionUtils.isEmpty(line.getChildren())) { + return; + } + Long parentUid = line.getUniqueId() != null ? line.getUniqueId() : (dbLine != null ? dbLine.getUniqueId() : null); + List realOnly = new ArrayList<>(); + for (ReceiptMaterialDetailDO c : line.getChildren()) { + if (c == null) { + continue; + } + if (parentUid != null && parentUid.equals(c.getUniqueId())) { + continue; + } + realOnly.add(c); + } + line.setChildren(realOnly); + } + + /** getInfoByApp 挂在 children 里的已落库子行,再次收货时需去掉,否则 inject 误判已有子项 */ + private static void stripPdaExistingDisplayedChildrenForResplit(ReceiptMaterialDetailDO line, ReceiptMaterialDetailPO dbLine, + List allDbLines) { + if (line == null || CollectionUtils.isEmpty(line.getChildren()) || dbLine == null || CollectionUtils.isEmpty(allDbLines)) { + return; + } + Long puid = dbLine.getUniqueId(); + Long pInUid = dbLine.getInUniqueId(); + List keep = new ArrayList<>(); + for (ReceiptMaterialDetailDO c : line.getChildren()) { + if (c == null) { + continue; + } + Long mid = c.getMaterialDetailId(); + Long cuid = c.getUniqueId(); + boolean isExistingDbChild = false; + for (ReceiptMaterialDetailPO x : allDbLines) { + if (x == null || x.getLevel() == null || x.getLevel() != 2 || x.getParentUniqueId() == null) { + continue; + } + boolean parentOk = (puid != null && puid.equals(x.getParentUniqueId())) + || (pInUid != null && pInUid.equals(x.getParentUniqueId())); + if (!parentOk) { + continue; + } + if (mid != null && mid.equals(x.getMaterialDetailId())) { + isExistingDbChild = true; + break; + } + if (cuid != null && cuid.equals(x.getUniqueId())) { + isExistingDbChild = true; + break; + } + } + if (!isExistingDbChild) { + keep.add(c); + } + } + line.setChildren(keep); + } + + private static boolean hasPdaInjectMetricIncrement(ReceiptMaterialDetailDO line, ReceiptMaterialDetailPO dbLine) { + if (line == null || dbLine == null) { + return false; + } + BigDecimal dbNet = dbLine.getAlreadyReceiptNetWeight() != null ? dbLine.getAlreadyReceiptNetWeight() : BigDecimal.ZERO; + BigDecimal dbGross = dbLine.getAlreadyReceiptGrossWeight() != null ? dbLine.getAlreadyReceiptGrossWeight() : BigDecimal.ZERO; + BigDecimal dbVol = dbLine.getAlreadyReceiptVolume() != null ? dbLine.getAlreadyReceiptVolume() : BigDecimal.ZERO; + BigDecimal dbArea = dbLine.getAlreadyReceiptArea() != null ? dbLine.getAlreadyReceiptArea() : BigDecimal.ZERO; + return pickThisReceiptMetricIncrement(line.getReceiptNetWeight(), line.getTotalNetWeight(), dbNet).compareTo(BigDecimal.ZERO) > 0 + || pickThisReceiptMetricIncrement(line.getReceiptGrossWeight(), line.getTotalGrossWeight(), dbGross).compareTo(BigDecimal.ZERO) > 0 + || pickThisReceiptMetricIncrement(line.getReceiptVolume(), line.getTotalVolume(), dbVol).compareTo(BigDecimal.ZERO) > 0 + || pickThisReceiptMetricIncrement(line.getReceiptArea(), line.getTotalArea(), dbArea).compareTo(BigDecimal.ZERO) > 0; + } + + /** + * PDA 单次仅传主行(无真实子结构需拆时):按业务可拆成一条 level=2 子行。 + * 首笔仅落主行不拆;第二笔起或已有子行再拆。脏主行(误标 level=2)纠正不受「首笔不拆」限制。 + */ + private void injectPdaFlatReceiptAsSyntheticChild(ReceiptMaterialDetailDO line, ReceiptMaterialDetailPO dbLine, + List allDbLines) { + if (line == null || dbLine == null) { + return; + } + stripPdaUiParentCopyChildren(line, dbLine); + stripPdaExistingDisplayedChildrenForResplit(line, dbLine, allDbLines); + if (dbLine.getLevel() != null && dbLine.getLevel() == 2 && !isCorruptedStandaloneLineAsLevel2(dbLine)) { + return; + } + if (!CollectionUtils.isEmpty(line.getChildren())) { + return; + } + if (!shouldPdaSplitIntoChildRow(dbLine, allDbLines) && !isCorruptedStandaloneLineAsLevel2(dbLine)) { + return; + } + BigDecimal rq = line.getReceiptQuantity(); + boolean hasQty = rq != null && rq.compareTo(BigDecimal.ZERO) > 0; + boolean hasMetricBatch = hasPdaInjectMetricIncrement(line, dbLine); + if (!hasQty && !hasMetricBatch) { + return; + } + ReceiptMaterialDetailDO child = new ReceiptMaterialDetailDO(); + BeanUtils.copyProperties(line, child, "children", "materialDetailId", "uniqueId", "level", "parentUniqueId"); + child.setUniqueId(idGenerator.snowflakeId(SnowFlakeConstants.wmsId)); + child.setChildren(null); + child.setLevel(2); + child.setParentUniqueId(resolveParentUniqueIdForPdaInjectChild(dbLine, allDbLines)); + if (hasQty) { + child.setReceiptQuantity(rq); + } + child.setReceiptGrossWeight(line.getReceiptGrossWeight()); + child.setReceiptNetWeight(line.getReceiptNetWeight()); + child.setReceiptVolume(line.getReceiptVolume()); + child.setReceiptArea(line.getReceiptArea()); + child.setTotalGrossWeight(line.getTotalGrossWeight()); + child.setTotalNetWeight(line.getTotalNetWeight()); + child.setTotalVolume(line.getTotalVolume()); + child.setTotalArea(line.getTotalArea()); + child.setAlreadyReceiptQuantity(null); + child.setAlreadyReceiptGrossWeight(null); + child.setAlreadyReceiptNetWeight(null); + child.setAlreadyReceiptVolume(null); + child.setAlreadyReceiptArea(null); + line.setChildren(Collections.singletonList(child)); + line.setReceiptQuantity(BigDecimal.ZERO); + line.setReceiptGrossWeight(null); + line.setReceiptNetWeight(null); + line.setReceiptVolume(null); + line.setReceiptArea(null); + line.setTotalGrossWeight(null); + line.setTotalNetWeight(null); + line.setTotalVolume(null); + line.setTotalArea(null); + line.setLevel(1); + line.setParentUniqueId(null); + } + /** * 收货时:将 children 中第一条的值赋给主项,并删除第一条。 * PDA 收两条时只生成 2 条(主项 + 第二条子项),主项展示第一条子项的数据。 @@ -1706,6 +2242,65 @@ public class StockReceiptOrderApplicationService { return totalField; } + private static boolean isApproxEqualReceiptMetric(BigDecimal a, BigDecimal b) { + if (a == null && b == null) { + return true; + } + if (a == null || b == null) { + return false; + } + return a.subtract(b).abs().compareTo(new BigDecimal("0.000001")) <= 0; + } + + /** + * PDA:详情里 total* 为「待收」= 计划 − 组内主项+子项全部已收;提交时若仍等于当前组剩余指标, + * 则按本行本次收货件数占「组内待收件数」比例分摊本次毛/净重/体积/面积增量。 + * 否则按 {@link #pickThisReceiptMetricIncrement}(累计实绩)处理。 + */ + private static BigDecimal pickThisReceiptMetricIncrementPda( + BigDecimal receiptField, + BigDecimal totalField, + BigDecimal dbSingleRowAlready, + ReceiptMaterialDetailPO dbLine, + List dbList, + Function planMetricGetter, + Function alreadyMetricGetter, + BigDecimal thisReceiptQty) { + if (receiptField != null) { + return receiptField; + } + if (totalField == null) { + return BigDecimal.ZERO; + } + if (dbLine == null || CollectionUtils.isEmpty(dbList) || thisReceiptQty == null + || thisReceiptQty.compareTo(BigDecimal.ZERO) <= 0) { + return pickThisReceiptMetricIncrement(receiptField, totalField, dbSingleRowAlready); + } + ReceiptMaterialDetailPO plan = resolveReceiptPlanParentRow(dbLine, dbList); + if (plan == null) { + return pickThisReceiptMetricIncrement(receiptField, totalField, dbSingleRowAlready); + } + BigDecimal planMetric = planMetricGetter.apply(plan); + if (planMetric == null) { + return pickThisReceiptMetricIncrement(receiptField, totalField, dbSingleRowAlready); + } + BigDecimal sumAlready = sumAlreadyMetricUnderPlanParent(plan, dbList, alreadyMetricGetter); + BigDecimal remainingMetric = planMetric.subtract(sumAlready); + if (remainingMetric.compareTo(BigDecimal.ZERO) < 0) { + remainingMetric = BigDecimal.ZERO; + } + BigDecimal planQty = plan.getQuantity() != null ? plan.getQuantity() : BigDecimal.ZERO; + BigDecimal sumAlreadyQty = sumAlreadyReceiptUnderPlanParent(plan, dbList); + BigDecimal pendingQty = planQty.subtract(sumAlreadyQty); + if (pendingQty.compareTo(BigDecimal.ZERO) <= 0) { + return pickThisReceiptMetricIncrement(receiptField, totalField, dbSingleRowAlready); + } + if (isApproxEqualReceiptMetric(totalField, remainingMetric)) { + return remainingMetric.multiply(thisReceiptQty).divide(pendingQty, 8, RoundingMode.HALF_UP); + } + return pickThisReceiptMetricIncrement(receiptField, totalField, dbSingleRowAlready); + } + /** * PC 收货确认:页面提交为「当前全量实绩」,直接取 receipt* / total* 落库,不与历史累计做差或相加。 */ @@ -1979,14 +2574,25 @@ public class StockReceiptOrderApplicationService { ? receiptMaterialDetailPO.getAlreadyReceiptNetWeight() : BigDecimal.ZERO; BigDecimal dbParentGrossForGate = receiptMaterialDetailPO.getAlreadyReceiptGrossWeight() != null ? receiptMaterialDetailPO.getAlreadyReceiptGrossWeight() : BigDecimal.ZERO; - boolean hasReceiptByMetrics = pickThisReceiptMetricIncrement( + BigDecimal gateParentQty = receiptQuantity; + boolean hasReceiptByMetrics = pickThisReceiptMetricIncrementPda( receiptMaterialDetailDO.getReceiptNetWeight(), receiptMaterialDetailDO.getTotalNetWeight(), - dbParentNetForGate).compareTo(BigDecimal.ZERO) > 0 - || pickThisReceiptMetricIncrement( + dbParentNetForGate, + receiptMaterialDetailPO, + receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanNetWeight, + ReceiptMaterialDetailPO::getAlreadyReceiptNetWeight, + gateParentQty).compareTo(BigDecimal.ZERO) > 0 + || pickThisReceiptMetricIncrementPda( receiptMaterialDetailDO.getReceiptGrossWeight(), receiptMaterialDetailDO.getTotalGrossWeight(), - dbParentGrossForGate).compareTo(BigDecimal.ZERO) > 0; + dbParentGrossForGate, + receiptMaterialDetailPO, + receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanGrossWeight, + ReceiptMaterialDetailPO::getAlreadyReceiptGrossWeight, + gateParentQty).compareTo(BigDecimal.ZERO) > 0; if (!hasReceiptByMetrics && !CollectionUtils.isEmpty(children)) { for (ReceiptMaterialDetailDO c : children) { if (c == null) { @@ -2000,8 +2606,19 @@ public class StockReceiptOrderApplicationService { ? cdb.getAlreadyReceiptNetWeight() : BigDecimal.ZERO; BigDecimal cDbGross = cdb != null && cdb.getAlreadyReceiptGrossWeight() != null ? cdb.getAlreadyReceiptGrossWeight() : BigDecimal.ZERO; - if (pickThisReceiptMetricIncrement(c.getReceiptNetWeight(), c.getTotalNetWeight(), cDbNet).compareTo(BigDecimal.ZERO) > 0 - || pickThisReceiptMetricIncrement(c.getReceiptGrossWeight(), c.getTotalGrossWeight(), cDbGross).compareTo(BigDecimal.ZERO) > 0) { + BigDecimal gateChildQty = c.getReceiptQuantity() != null ? c.getReceiptQuantity() : BigDecimal.ZERO; + if (pickThisReceiptMetricIncrementPda( + c.getReceiptNetWeight(), c.getTotalNetWeight(), cDbNet, cdb, + receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanNetWeight, + ReceiptMaterialDetailPO::getAlreadyReceiptNetWeight, + gateChildQty).compareTo(BigDecimal.ZERO) > 0 + || pickThisReceiptMetricIncrementPda( + c.getReceiptGrossWeight(), c.getTotalGrossWeight(), cDbGross, cdb, + receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanGrossWeight, + ReceiptMaterialDetailPO::getAlreadyReceiptGrossWeight, + gateChildQty).compareTo(BigDecimal.ZERO) > 0) { hasReceiptByMetrics = true; break; } @@ -2044,10 +2661,22 @@ public class StockReceiptOrderApplicationService { BigDecimal childDbAlreadyArea = receiptMaterialDetailPOChildDb != null && receiptMaterialDetailPOChildDb.getAlreadyReceiptArea() != null ? receiptMaterialDetailPOChildDb.getAlreadyReceiptArea() : BigDecimal.ZERO; BigDecimal childQty = receiptMaterialDetailDOChild.getReceiptQuantity() != null ? receiptMaterialDetailDOChild.getReceiptQuantity() : BigDecimal.ZERO; - BigDecimal childGross = pickThisReceiptMetricIncrement(receiptMaterialDetailDOChild.getReceiptGrossWeight(), receiptMaterialDetailDOChild.getTotalGrossWeight(), childDbAlreadyGross); - BigDecimal childNet = pickThisReceiptMetricIncrement(receiptMaterialDetailDOChild.getReceiptNetWeight(), receiptMaterialDetailDOChild.getTotalNetWeight(), childDbAlreadyNet); - BigDecimal childVol = pickThisReceiptMetricIncrement(receiptMaterialDetailDOChild.getReceiptVolume(), receiptMaterialDetailDOChild.getTotalVolume(), childDbAlreadyVol); - BigDecimal childArea = pickThisReceiptMetricIncrement(receiptMaterialDetailDOChild.getReceiptArea(), receiptMaterialDetailDOChild.getTotalArea(), childDbAlreadyArea); + BigDecimal childGross = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDOChild.getReceiptGrossWeight(), receiptMaterialDetailDOChild.getTotalGrossWeight(), + childDbAlreadyGross, receiptMaterialDetailPOChildDb, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanGrossWeight, ReceiptMaterialDetailPO::getAlreadyReceiptGrossWeight, childQty); + BigDecimal childNet = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDOChild.getReceiptNetWeight(), receiptMaterialDetailDOChild.getTotalNetWeight(), + childDbAlreadyNet, receiptMaterialDetailPOChildDb, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanNetWeight, ReceiptMaterialDetailPO::getAlreadyReceiptNetWeight, childQty); + BigDecimal childVol = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDOChild.getReceiptVolume(), receiptMaterialDetailDOChild.getTotalVolume(), + childDbAlreadyVol, receiptMaterialDetailPOChildDb, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanVolume, ReceiptMaterialDetailPO::getAlreadyReceiptVolume, childQty); + BigDecimal childArea = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDOChild.getReceiptArea(), receiptMaterialDetailDOChild.getTotalArea(), + childDbAlreadyArea, receiptMaterialDetailPOChildDb, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanArea, ReceiptMaterialDetailPO::getAlreadyReceiptArea, childQty); // PC 端子行常只回填 totalNetWeight/totalGrossWeight,不回传 receiptQuantity;用本次称重增量兜底为件数(与千克等重量计量物料一致) if (childQty.compareTo(BigDecimal.ZERO) == 0 && childNet.compareTo(BigDecimal.ZERO) > 0) { childQty = childNet; @@ -2126,10 +2755,22 @@ public class StockReceiptOrderApplicationService { BigDecimal dbParentBeforeNet = receiptMaterialDetailPO.getAlreadyReceiptNetWeight() != null ? receiptMaterialDetailPO.getAlreadyReceiptNetWeight() : BigDecimal.ZERO; BigDecimal dbParentBeforeVol = receiptMaterialDetailPO.getAlreadyReceiptVolume() != null ? receiptMaterialDetailPO.getAlreadyReceiptVolume() : BigDecimal.ZERO; BigDecimal dbParentBeforeArea = receiptMaterialDetailPO.getAlreadyReceiptArea() != null ? receiptMaterialDetailPO.getAlreadyReceiptArea() : BigDecimal.ZERO; - BigDecimal parentReceiptGross = pickThisReceiptMetricIncrement(receiptMaterialDetailDO.getReceiptGrossWeight(), receiptMaterialDetailDO.getTotalGrossWeight(), dbParentBeforeGross); - BigDecimal parentReceiptNet = pickThisReceiptMetricIncrement(receiptMaterialDetailDO.getReceiptNetWeight(), receiptMaterialDetailDO.getTotalNetWeight(), dbParentBeforeNet); - BigDecimal parentReceiptVol = pickThisReceiptMetricIncrement(receiptMaterialDetailDO.getReceiptVolume(), receiptMaterialDetailDO.getTotalVolume(), dbParentBeforeVol); - BigDecimal parentReceiptArea = pickThisReceiptMetricIncrement(receiptMaterialDetailDO.getReceiptArea(), receiptMaterialDetailDO.getTotalArea(), dbParentBeforeArea); + BigDecimal parentReceiptGross = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDO.getReceiptGrossWeight(), receiptMaterialDetailDO.getTotalGrossWeight(), + dbParentBeforeGross, receiptMaterialDetailPO, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanGrossWeight, ReceiptMaterialDetailPO::getAlreadyReceiptGrossWeight, mainItemActualQuantity); + BigDecimal parentReceiptNet = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDO.getReceiptNetWeight(), receiptMaterialDetailDO.getTotalNetWeight(), + dbParentBeforeNet, receiptMaterialDetailPO, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanNetWeight, ReceiptMaterialDetailPO::getAlreadyReceiptNetWeight, mainItemActualQuantity); + BigDecimal parentReceiptVol = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDO.getReceiptVolume(), receiptMaterialDetailDO.getTotalVolume(), + dbParentBeforeVol, receiptMaterialDetailPO, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanVolume, ReceiptMaterialDetailPO::getAlreadyReceiptVolume, mainItemActualQuantity); + BigDecimal parentReceiptArea = pickThisReceiptMetricIncrementPda( + receiptMaterialDetailDO.getReceiptArea(), receiptMaterialDetailDO.getTotalArea(), + dbParentBeforeArea, receiptMaterialDetailPO, receiptMaterialDetailPODbList, + ReceiptMaterialDetailPO::getPlanArea, ReceiptMaterialDetailPO::getAlreadyReceiptArea, mainItemActualQuantity); // 主行 already_* 只累计主行本行毛净重体面积;子行各自落子行记录,避免主行重复叠加子行导致与数量口径不一致 BigDecimal mainGrossIncrement = parentReceiptGross; BigDecimal mainNetIncrement = parentReceiptNet; @@ -2187,6 +2828,11 @@ public class StockReceiptOrderApplicationService { ReceiptMaterialDetailDO returnReceiptMaterialDetailDO = new ReceiptMaterialDetailDO(); BeanUtils.copyProperties(receiptMaterialDetailPO, returnReceiptMaterialDetailDO); BeanUtils.copyProperties(receiptMaterialDetailDO, returnReceiptMaterialDetailDO, IgnoreNullUtil.getNullPropertyNames(receiptMaterialDetailDO)); + // 主行本次收货件数为 0、仅子行有收货时:不从请求覆盖主行批次号(避免 PDA 把子行新批次回写到主行,冲掉首笔批次) + if (mainItemActualQuantity.compareTo(BigDecimal.ZERO) == 0 + && !CollectionUtils.isEmpty(returnReceiptMaterialDetailDOChildList)) { + returnReceiptMaterialDetailDO.setBatchNumber(receiptMaterialDetailPO.getBatchNumber()); + } returnReceiptMaterialDetailDO.setTotalNetWeight(receiptMaterialDetailDO.getTotalNetWeight()); returnReceiptMaterialDetailDO.setTotalGrossWeight(receiptMaterialDetailDO.getTotalGrossWeight()); returnReceiptMaterialDetailDO.setTotalVolume(receiptMaterialDetailDO.getTotalVolume()); diff --git a/mhd_wms/src/main/java/com/mhd/wms/application/service/stockShelfOrder/StockShelfOrderApplicationService.java b/mhd_wms/src/main/java/com/mhd/wms/application/service/stockShelfOrder/StockShelfOrderApplicationService.java index b4db95550..21906dc2e 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/application/service/stockShelfOrder/StockShelfOrderApplicationService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/application/service/stockShelfOrder/StockShelfOrderApplicationService.java @@ -57,9 +57,11 @@ import org.springframework.util.CollectionUtils; import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -204,31 +206,17 @@ public class StockShelfOrderApplicationService { /** * 获取上架单详细信息 + * * @param shelfOrderId 上架单ID - * @param addParentCopyWhenNoChildren 主项无子项时是否添加 parent_copy:true-PDA 详情页从 children 读取数据需要;false-PC 端收货无子项时上架不显示虚拟子项 + * @param addParentCopyWhenNoChildren 历史参数名保留:为 true 时表示走 {@link #getInfo(Long, boolean, Integer)} 中的 PDA 详情流水线(扁平化、待上架、重量展示等);不再向 children 注入 + * parent_copy,避免与主行重复两条;无子项时由前端从主项字段读取,与 {@link com.mhd.wms.domain.stockShelfOrder.service.StockShelfOrderDomainService#getInfoByApp(Long)} 一致。为 false 时走 PC 端汇总与扁平化。 */ public StockShelfOrderPO getInfo(Long shelfOrderId, boolean addParentCopyWhenNoChildren) { StockShelfOrderPO stockShelfOrderPO = stockShelfOrderDomainService.getInfo(shelfOrderId); ShelfMaterialDetailDO shelfMaterialDetailDO = new ShelfMaterialDetailDO(); shelfMaterialDetailDO.setShelfOrderNumber(stockShelfOrderPO.getShelfOrderNumber()); List shelfMaterialDetailPOList = shelfMaterialDetailDomainService.queryListChildren(shelfMaterialDetailDO); - // PDA:主项无子项时添加 parent_copy 到 children,供详情页展示(详情页从 children 读取数据);PC:不添加,避免收货无子项时上架显示虚拟子项 - if (addParentCopyWhenNoChildren) { - for (ShelfMaterialDetailPO parent : shelfMaterialDetailPOList) { - if (CollectionUtils.isEmpty(parent.getChildren())) { - ShelfMaterialDetailPO parentCopy = new ShelfMaterialDetailPO(); - BeanUtils.copyProperties(parent, parentCopy, "children"); - parentCopy.setLevel(2); - parentCopy.setParentUniqueId(parent.getUniqueId()); - parentCopy.setQuantity(null); - parentCopy.setChildren(new ArrayList<>()); - List children = new ArrayList<>(); - children.add(parentCopy); - parent.setChildren(children); - } - } - } - + // 性能优化:批量获取批次属性,避免N+1查询问题 // 1. 收集所有唯一的materialBaseInfoId Set materialBaseInfoIdSet = shelfMaterialDetailPOList.stream() @@ -322,11 +310,42 @@ public class StockShelfOrderApplicationService { // 第一次上架:主项无子项时,上架数量默认等于收货数量(quantity 由上架单生成时取自收货数量),供 PDA 表单默认展示 fillFirstShelvesQuantityDefault(shelfMaterialDetailPOList); - + + // PC 端(addParentCopyWhenNoChildren==false):与收货单 getInfo 相同流水线——先按行递归汇总部净重/毛重/体面积,再扁平化主行、清空子行计划数量、待上架数量,最后按「计划−已上架」覆盖 total* 展示(对齐收货详情计算口径) + if (!addParentCopyWhenNoChildren) { + BigDecimal totalNetWeight = BigDecimal.ZERO; + BigDecimal totalGrossWeight = BigDecimal.ZERO; + BigDecimal totalVolume = BigDecimal.ZERO; + BigDecimal totalArea = BigDecimal.ZERO; + if (!CollectionUtils.isEmpty(shelfMaterialDetailPOList)) { + for (ShelfMaterialDetailPO detail : shelfMaterialDetailPOList) { + totalNetWeight = totalNetWeight.add(sumShelfDetailRecursively(detail, ShelfMaterialDetailPO::getTotalNetWeight)); + totalGrossWeight = totalGrossWeight.add(sumShelfDetailRecursively(detail, ShelfMaterialDetailPO::getTotalGrossWeight)); + totalVolume = totalVolume.add(sumShelfDetailRecursively(detail, ShelfMaterialDetailPO::getTotalVolume)); + totalArea = totalArea.add(sumShelfDetailRecursively(detail, ShelfMaterialDetailPO::getTotalArea)); + } + } + stockShelfOrderPO.setTotalNetWeight(totalNetWeight); + stockShelfOrderPO.setTotalGrossWeight(totalGrossWeight); + stockShelfOrderPO.setTotalVolume(totalVolume); + stockShelfOrderPO.setTotalArea(totalArea); + boolean shelfOrderCompleted = stockShelfOrderPO.getStatus() != null && stockShelfOrderPO.getStatus() == 3; + flattenShelfMaterialDetailList(shelfMaterialDetailPOList, shelfOrderCompleted); + clearChildQuantityShelfRecursively(shelfMaterialDetailPOList); + fillPendingShelvesQuantityRecursively(shelfMaterialDetailPOList); + Map planWeightMetrics = loadReceiptPlanWeightMetrics(stockShelfOrderPO.getReceiptOrderNumber()); + overwriteTotalWithPendingForPdaWeightDisplayRecursively(shelfMaterialDetailPOList, planWeightMetrics); + syncMaterialMoreDetailWeightValuesRecursively(shelfMaterialDetailPOList); + } + stockShelfOrderPO.setMaterialDetailList(shelfMaterialDetailPOList); - log.info("返回上架单信息,shelfOrderId={}, materialDetailList大小={}", - stockShelfOrderPO.getShelfOrderId(), - shelfMaterialDetailPOList != null ? shelfMaterialDetailPOList.size() : 0); + log.info("返回上架单信息,shelfOrderId={}, materialDetailList大小={}, totalNetWeight={}, totalGrossWeight={}, totalVolume={}, totalArea={}", + stockShelfOrderPO.getShelfOrderId(), + shelfMaterialDetailPOList != null ? shelfMaterialDetailPOList.size() : 0, + stockShelfOrderPO.getTotalNetWeight(), + stockShelfOrderPO.getTotalGrossWeight(), + stockShelfOrderPO.getTotalVolume(), + stockShelfOrderPO.getTotalArea()); return stockShelfOrderPO; } @@ -351,9 +370,12 @@ public class StockShelfOrderApplicationService { if (shelfStatus != null) { materialDetailList = filterShelfMaterialDetailsByShelfStatus(materialDetailList, shelfStatus); } - // 已上架完成(3/4/5):主项无子项时注入的 parent_copy 与主项为同一明细,子行可能仍带「本次提交」的 shelves_quantity=0, - // 导致「上架数量」误显示为 0;与主项对齐累计已上架数量后再做展示计算 + // 兼容历史:若 children 中仍有镜像子行(与主项同 uniqueId),已收尾时与主项对齐累计已上架,再扁平化/剔除镜像 alignParentCopyShelfQuantityWithParentForTerminal(materialDetailList); + boolean shelfOrderCompleted = stockShelfOrderPO.getStatus() != null && stockShelfOrderPO.getStatus() == 3; + flattenShelfMaterialDetailList(materialDetailList, shelfOrderCompleted); + // 与域层 getInfoByApp 一致:不再注入 parent_copy;若旧客户端缓存等仍带镜像子行,从树中剔除以免详情列表出现两条 + removeShelfParentCopyMirrorChildrenRecursively(materialDetailList); // PDA 待上架数量 = 计划数量 - 已上架数量,仅接口计算(不落库) fillPendingShelvesQuantityRecursively(materialDetailList); // PDA 展示:shelvesQuantity 显示为累计已上架数量(与 receipt 口径一致);已上架完成行不再用本次提交量 @@ -364,10 +386,19 @@ public class StockShelfOrderApplicationService { Map planWeightMetrics = loadReceiptPlanWeightMetrics(stockShelfOrderPO.getReceiptOrderNumber()); overwriteTotalWithPendingForPdaWeightDisplayRecursively(materialDetailList, planWeightMetrics); syncMaterialMoreDetailWeightValuesRecursively(materialDetailList); + // 与收货 PDA getInfoByApp 一致:保留 children;仅清空子行「计划数量」quantity(见 clearChildQuantityRecursively),不在此剔除分库位 level=2 子行 + clearChildQuantityShelfRecursively(materialDetailList); stockShelfOrderPO.setMaterialDetailList(materialDetailList); return stockShelfOrderPO; } + /** + * PDA:按父计划行查询分批上架子行(level=2),与收货 {@link com.mhd.wms.application.service.stockReceiptOrder.StockReceiptOrderApplicationService#listLevel2ChildrenByParentForApp} 对称。 + */ + public List listLevel2ChildrenByParentForApp(Long shelfOrderId, Long parentMaterialDetailId) { + return stockShelfOrderDomainService.listLevel2ChildrenByParentForApp(shelfOrderId, parentMaterialDetailId); + } + private List filterShelfMaterialDetailsByShelfStatus(List source, Integer shelfStatus) { if (CollectionUtils.isEmpty(source) || shelfStatus == null) { return source; @@ -399,6 +430,46 @@ public class StockShelfOrderApplicationService { return true; } + /** + * 递归汇总明细及其子项的某字段值(与收货 {@link com.mhd.wms.application.service.stockReceiptOrder.StockReceiptOrderApplicationService} 中 + * sumDetailRecursively 一致,用于 PC 单头 total*) + */ + private static BigDecimal sumShelfDetailRecursively(ShelfMaterialDetailPO detail, Function getter) { + BigDecimal sum = BigDecimal.ZERO; + if (detail == null || getter == null) { + return sum; + } + BigDecimal v = getter.apply(detail); + if (v != null) { + sum = sum.add(v); + } + List children = detail.getChildren(); + if (!CollectionUtils.isEmpty(children)) { + for (ShelfMaterialDetailPO child : children) { + sum = sum.add(sumShelfDetailRecursively(child, getter)); + } + } + return sum; + } + + /** + * PC 端子项计划数量不展示:递归清空所有子项的 quantity(与收货 clearChildQuantityRecursively 一致) + */ + private void clearChildQuantityShelfRecursively(List shelfMaterialDetailPOList) { + if (CollectionUtils.isEmpty(shelfMaterialDetailPOList)) { + return; + } + for (ShelfMaterialDetailPO detail : shelfMaterialDetailPOList) { + List children = detail.getChildren(); + if (!CollectionUtils.isEmpty(children)) { + for (ShelfMaterialDetailPO child : children) { + child.setQuantity(null); + clearChildQuantityShelfRecursively(child.getChildren()); + } + } + } + } + /** * PDA 待上架数量递归计算:待上架数量 = max(0, 计划数量 - 已上架数量) */ @@ -450,6 +521,396 @@ public class StockShelfOrderApplicationService { } } + /** + * 扁平化明细:主行保留父子结构;展示「已上架」= 主行累计 + 各子行累计(不含 parent_copy 镜像行,避免与主行重复)。 + * 上架状态按「汇总已上架 qtySum」与主行计划量比较推导,与域服务分批/完成上架的 1/2/3/4/5 含义一致。 + * + * @param shelfOrderCompleted 上架单是否已整单完成(status=3);为 true 时少上为 4,与域服务完成口径一致 + */ + private void flattenShelfMaterialDetailList(List shelfMaterialDetailPOList, + boolean shelfOrderCompleted) { + if (CollectionUtils.isEmpty(shelfMaterialDetailPOList)) { + return; + } + List allNodes = new ArrayList<>(); + collectShelfMaterialDetailNodesRecursively(shelfMaterialDetailPOList, allNodes); + Map> childrenByParentUid = new HashMap<>(); + for (ShelfMaterialDetailPO node : allNodes) { + if (node == null || node.getLevel() == null || node.getLevel() != 2) { + continue; + } + Long puid = node.getParentUniqueId(); + if (puid == null || Long.valueOf(0).equals(puid)) { + continue; + } + childrenByParentUid.computeIfAbsent(puid, k -> new ArrayList<>()).add(node); + } + for (ShelfMaterialDetailPO parent : shelfMaterialDetailPOList) { + if (parent == null) { + continue; + } + if (parent.getLevel() != null && parent.getLevel() != 1) { + continue; + } + BigDecimal qtySum = sumAlreadyShelvesUnderPlanParent(parent, allNodes); + BigDecimal netSum = sumAlreadyShelfMetricUnderPlanParent(parent, allNodes, ShelfMaterialDetailPO::getAlreadyShelvesNetWeight); + BigDecimal grossSum = sumAlreadyShelfMetricUnderPlanParent(parent, allNodes, ShelfMaterialDetailPO::getAlreadyShelvesGrossWeight); + BigDecimal volSum = sumAlreadyShelfMetricUnderPlanParent(parent, allNodes, ShelfMaterialDetailPO::getAlreadyShelvesVolume); + BigDecimal areaSum = sumAlreadyShelfMetricUnderPlanParent(parent, allNodes, ShelfMaterialDetailPO::getAlreadyShelvesArea); + Integer derived = deriveShelfStatusFromAggregatedVsPlan(qtySum, parent.getQuantity(), shelfOrderCompleted); + + parent.setAlreadyShelvesQuantity(qtySum); + parent.setAlreadyShelvesNetWeight(netSum); + parent.setAlreadyShelvesGrossWeight(grossSum); + parent.setAlreadyShelvesVolume(volSum); + parent.setAlreadyShelvesArea(areaSum); + if (derived != null) { + parent.setShelvesStatus(derived); + } + + LinkedHashMap mergedChildren = new LinkedHashMap<>(); + mergeShelfChildRowsIntoMap(mergedChildren, parent.getChildren()); + Long uid = parent.getUniqueId(); + Long inUid = parent.getInUniqueId(); + if (uid != null) { + mergeShelfChildRowsIntoMap(mergedChildren, childrenByParentUid.get(uid)); + } + if (inUid != null && !inUid.equals(uid)) { + mergeShelfChildRowsIntoMap(mergedChildren, childrenByParentUid.get(inUid)); + } + if (mergedChildren.isEmpty()) { + continue; + } + for (ShelfMaterialDetailPO child : mergedChildren.values()) { + if (child == null) { + continue; + } + if (parent.getUniqueId() != null && parent.getUniqueId().equals(child.getUniqueId())) { + child.setQuantity(null); + continue; + } + child.setQuantity(null); + if (derived != null && child.getLevel() != null && child.getLevel() == 2) { + child.setShelvesStatus(derived); + } + } + } + } + + /** + * 主行展示用上架状态:按「主行+子行汇总已上架」与主行计划量比较。 + */ + private static Integer deriveShelfStatusFromAggregatedVsPlan(BigDecimal aggregatedShelves, BigDecimal planQty, + boolean shelfOrderCompleted) { + BigDecimal r = aggregatedShelves != null ? aggregatedShelves : BigDecimal.ZERO; + BigDecimal p = planQty != null ? planQty : BigDecimal.ZERO; + if (shelfOrderCompleted) { + int cmp = r.compareTo(p); + if (cmp > 0) { + return 5; + } + if (cmp < 0) { + return 4; + } + return 3; + } + if (r.compareTo(BigDecimal.ZERO) == 0) { + return 1; + } + int cmp = r.compareTo(p); + if (cmp < 0) { + return 2; + } + if (cmp > 0) { + return 5; + } + return 3; + } + + private static void collectShelfMaterialDetailNodesRecursively(List list, List out) { + if (CollectionUtils.isEmpty(list)) { + return; + } + for (ShelfMaterialDetailPO n : list) { + if (n == null) { + continue; + } + out.add(n); + collectShelfMaterialDetailNodesRecursively(n.getChildren(), out); + } + } + + private static void mergeShelfChildRowsIntoMap(Map byKey, List children) { + if (CollectionUtils.isEmpty(children)) { + return; + } + for (ShelfMaterialDetailPO c : children) { + if (c == null) { + continue; + } + Long key = c.getUniqueId(); + if (key == null) { + key = c.getMaterialDetailId(); + } + if (key == null) { + continue; + } + byKey.putIfAbsent(key, c); + } + } + + private static boolean sameLongId(Long a, Long b) { + if (a == null || b == null) { + return false; + } + return a.longValue() == b.longValue(); + } + + private static boolean isLevel2ChildShelfRowUnderPlanParent(ShelfMaterialDetailPO planParent, ShelfMaterialDetailPO x) { + if (planParent == null || x == null) { + return false; + } + if (x.getLevel() == null || x.getLevel() != 2) { + return false; + } + Long puid = x.getParentUniqueId(); + if (puid == null || Long.valueOf(0).equals(puid)) { + return false; + } + Long uid = planParent.getUniqueId(); + Long inUid = planParent.getInUniqueId(); + boolean linked = (uid != null && sameLongId(uid, puid)) || (inUid != null && sameLongId(inUid, puid)); + if (!linked) { + return false; + } + if (planParent.getUniqueId() != null && sameLongId(planParent.getUniqueId(), x.getUniqueId())) { + return false; + } + return true; + } + + private static BigDecimal sumAlreadyShelvesUnderPlanParent(ShelfMaterialDetailPO planParent, + List dbList) { + if (planParent == null || CollectionUtils.isEmpty(dbList)) { + return BigDecimal.ZERO; + } + BigDecimal sum = planParent.getAlreadyShelvesQuantity() != null + ? planParent.getAlreadyShelvesQuantity() : BigDecimal.ZERO; + for (ShelfMaterialDetailPO x : dbList) { + if (x == null || !isLevel2ChildShelfRowUnderPlanParent(planParent, x)) { + continue; + } + BigDecimal ar = x.getAlreadyShelvesQuantity() != null ? x.getAlreadyShelvesQuantity() : BigDecimal.ZERO; + sum = sum.add(ar); + } + return sum; + } + + private static BigDecimal sumAlreadyShelfMetricUnderPlanParent(ShelfMaterialDetailPO planParent, + List dbList, + Function alreadyGetter) { + if (planParent == null || CollectionUtils.isEmpty(dbList) || alreadyGetter == null) { + return BigDecimal.ZERO; + } + BigDecimal base = alreadyGetter.apply(planParent); + BigDecimal sum = base != null ? base : BigDecimal.ZERO; + for (ShelfMaterialDetailPO x : dbList) { + if (x == null || !isLevel2ChildShelfRowUnderPlanParent(planParent, x)) { + continue; + } + BigDecimal v = alreadyGetter.apply(x); + sum = sum.add(v != null ? v : BigDecimal.ZERO); + } + return sum; + } + + /** 计划父行:level=1 自身;level=2 则按 parent_unique_id 定位主计划行(对齐收货 {@code resolveReceiptPlanParentRow}) */ + private static ShelfMaterialDetailPO resolveShelfPlanParentRow(ShelfMaterialDetailPO db, + List dbList) { + if (db == null || CollectionUtils.isEmpty(dbList)) { + return db; + } + if (db.getLevel() == null || db.getLevel() == 1) { + return db; + } + if (db.getLevel() == 2 && db.getParentUniqueId() != null) { + Long puid = db.getParentUniqueId(); + for (ShelfMaterialDetailPO x : dbList) { + if (x == null) { + continue; + } + if (puid.equals(x.getUniqueId()) || puid.equals(x.getInUniqueId())) { + return x; + } + } + } + return db; + } + + /** + * 首次可只落主行不拆;已有 level=2 子行或主行+子行已有累计已上架时再拆(对齐收货 {@code shouldPdaSplitIntoChildRow}) + */ + private static boolean shouldPdaSplitIntoShelfChildRow(ShelfMaterialDetailPO dbLine, + List allDbLines) { + if (dbLine == null || CollectionUtils.isEmpty(allDbLines)) { + return true; + } + ShelfMaterialDetailPO plan = resolveShelfPlanParentRow(dbLine, allDbLines); + if (plan == null) { + return true; + } + Long uid = plan.getUniqueId(); + Long inUid = plan.getInUniqueId(); + boolean hasL2 = allDbLines.stream().anyMatch(x -> x != null && x.getLevel() != null && x.getLevel() == 2 + && x.getParentUniqueId() != null + && ((uid != null && uid.equals(x.getParentUniqueId())) + || (inUid != null && inUid.equals(x.getParentUniqueId())))); + if (hasL2) { + return true; + } + BigDecimal sum = sumAlreadyShelvesUnderPlanParent(plan, allDbLines); + return sum.compareTo(BigDecimal.ZERO) > 0; + } + + private static boolean isCorruptedStandaloneLineAsLevel2Shelf(ShelfMaterialDetailPO dbLine) { + if (dbLine == null || dbLine.getLevel() == null || dbLine.getLevel() != 2) { + return false; + } + return dbLine.getParentUniqueId() == null || Objects.equals(dbLine.getParentUniqueId(), 0L); + } + + private static void stripPdaUiParentCopyChildrenShelf(ShelfMaterialDetailDO line, ShelfMaterialDetailPO dbLine) { + if (line == null || CollectionUtils.isEmpty(line.getChildren())) { + return; + } + Long parentUid = line.getUniqueId() != null ? line.getUniqueId() : (dbLine != null ? dbLine.getUniqueId() : null); + List realOnly = new ArrayList<>(); + for (ShelfMaterialDetailDO c : line.getChildren()) { + if (c == null) { + continue; + } + if (parentUid != null && parentUid.equals(c.getUniqueId())) { + continue; + } + realOnly.add(c); + } + line.setChildren(realOnly); + } + + /** getInfo 挂在 children 里的已落库子行,再次上架时需去掉,否则无法走「拆新子行」注入(对齐收货 stripPdaExistingDisplayedChildrenForResplit) */ + private static void stripPdaExistingDisplayedChildrenForResplitShelf(ShelfMaterialDetailDO line, + ShelfMaterialDetailPO dbLine, + List allDbLines) { + if (line == null || CollectionUtils.isEmpty(line.getChildren()) || dbLine == null || CollectionUtils.isEmpty(allDbLines)) { + return; + } + Long puid = dbLine.getUniqueId(); + Long pInUid = dbLine.getInUniqueId(); + List keep = new ArrayList<>(); + for (ShelfMaterialDetailDO c : line.getChildren()) { + if (c == null) { + continue; + } + Long mid = c.getMaterialDetailId(); + Long cuid = c.getUniqueId(); + boolean isExistingDbChild = false; + for (ShelfMaterialDetailPO x : allDbLines) { + if (x == null || x.getLevel() == null || x.getLevel() != 2 || x.getParentUniqueId() == null) { + continue; + } + boolean parentOk = (puid != null && puid.equals(x.getParentUniqueId())) + || (pInUid != null && pInUid.equals(x.getParentUniqueId())); + if (!parentOk) { + continue; + } + if (mid != null && mid.equals(x.getMaterialDetailId())) { + isExistingDbChild = true; + break; + } + if (cuid != null && cuid.equals(x.getUniqueId())) { + isExistingDbChild = true; + break; + } + } + if (!isExistingDbChild) { + keep.add(c); + } + } + line.setChildren(keep); + } + + /** 本次上架毛/净重、体积、面积增量(对齐收货 pickThisReceiptMetricIncrement;上架无 receipt* 字段,用 total*) */ + private static BigDecimal pickThisShelfMetricIncrement(BigDecimal receiptField, BigDecimal totalField, + BigDecimal dbTotalAlreadyBefore) { + if (receiptField != null) { + return receiptField; + } + if (totalField == null) { + return BigDecimal.ZERO; + } + if (dbTotalAlreadyBefore == null) { + return totalField; + } + if (totalField.compareTo(dbTotalAlreadyBefore) == 0) { + return BigDecimal.ZERO; + } + if (totalField.compareTo(dbTotalAlreadyBefore) > 0) { + return totalField.subtract(dbTotalAlreadyBefore); + } + return totalField; + } + + private static boolean hasPdaShelfMetricIncrement(ShelfMaterialDetailDO line, ShelfMaterialDetailPO dbLine) { + if (line == null || dbLine == null) { + return false; + } + BigDecimal dbNet = dbLine.getTotalNetWeight() != null ? dbLine.getTotalNetWeight() : BigDecimal.ZERO; + BigDecimal dbGross = dbLine.getTotalGrossWeight() != null ? dbLine.getTotalGrossWeight() : BigDecimal.ZERO; + BigDecimal dbVol = dbLine.getTotalVolume() != null ? dbLine.getTotalVolume() : BigDecimal.ZERO; + BigDecimal dbArea = dbLine.getTotalArea() != null ? dbLine.getTotalArea() : BigDecimal.ZERO; + return pickThisShelfMetricIncrement(null, line.getTotalNetWeight(), dbNet).compareTo(BigDecimal.ZERO) > 0 + || pickThisShelfMetricIncrement(null, line.getTotalGrossWeight(), dbGross).compareTo(BigDecimal.ZERO) > 0 + || pickThisShelfMetricIncrement(null, line.getTotalVolume(), dbVol).compareTo(BigDecimal.ZERO) > 0 + || pickThisShelfMetricIncrement(null, line.getTotalArea(), dbArea).compareTo(BigDecimal.ZERO) > 0; + } + + private static Long resolveParentUniqueIdForPdaInjectChildShelf(ShelfMaterialDetailPO dbLine, + List allDbLines) { + if (dbLine == null) { + return null; + } + if (isCorruptedStandaloneLineAsLevel2Shelf(dbLine) && dbLine.getMaterialDetailId() != null + && !CollectionUtils.isEmpty(allDbLines)) { + for (ShelfMaterialDetailPO x : allDbLines) { + if (x != null && Objects.equals(dbLine.getMaterialDetailId(), x.getMaterialDetailId()) + && x.getLevel() != null && x.getLevel() == 1) { + return x.getUniqueId(); + } + } + } + ShelfMaterialDetailPO plan = resolveShelfPlanParentRow(dbLine, allDbLines); + return plan != null ? plan.getUniqueId() : dbLine.getUniqueId(); + } + + /** + * PDA 详情:从树上移除与主行同 material_detail_id / unique_id 的镜像子行(旧逻辑注入的 parent_copy),避免与主行并列两条。 + */ + private void removeShelfParentCopyMirrorChildrenRecursively(List list) { + if (CollectionUtils.isEmpty(list)) { + return; + } + for (ShelfMaterialDetailPO parent : list) { + if (parent == null) { + continue; + } + List ch = parent.getChildren(); + if (!CollectionUtils.isEmpty(ch)) { + ch.removeIf(c -> isShelfParentCopyPo(parent, c)); + removeShelfParentCopyMirrorChildrenRecursively(ch); + } + } + } + /** * 判断子项是否为 getInfo 时注入的 parent_copy(与主项同 materialDetailId 或 uniqueId) */ @@ -551,9 +1012,23 @@ public class StockShelfOrderApplicationService { return result; } + /** + * PDA/PC 详情:未完成行将 total* 覆盖为「待上架重量/体积」= 计划 − 累计已上架。 + * 与收货 {@link com.mhd.wms.application.service.stockReceiptOrder.StockReceiptOrderApplicationService#fillPlanAndPendingMetricsRecursively} 一致: + * level=2 子行计算时用计划主行(level=1)上已汇总的 alreadyShelves*,不能用子行本行累计, + * 否则「计划」是整张卡的计划,「已减」却只有子行一批,待上架会偏大。 + */ private void overwriteTotalWithPendingForPdaWeightDisplayRecursively( List list, Map planWeightMetrics + ) { + overwriteTotalWithPendingForPdaWeightDisplayRecursively(list, planWeightMetrics, null); + } + + private void overwriteTotalWithPendingForPdaWeightDisplayRecursively( + List list, + Map planWeightMetrics, + ShelfMaterialDetailPO planParent ) { if (CollectionUtils.isEmpty(list)) { return; @@ -562,6 +1037,10 @@ public class StockShelfOrderApplicationService { if (d == null) { continue; } + ShelfMaterialDetailPO nextPlanParent = planParent; + if (d.getLevel() != null && d.getLevel() == 1) { + nextPlanParent = d; + } // 已上架:不覆盖,展示累计已上架实绩(与 DB 同口径) if (isShelfTerminalShelvesStatus(d.getShelvesStatus())) { // 参考收货:已完成行不参与「计划−待收」,total* 直接展示累计 already @@ -577,7 +1056,7 @@ public class StockShelfOrderApplicationService { if (d.getAlreadyShelvesArea() != null) { d.setTotalArea(d.getAlreadyShelvesArea()); } - overwriteTotalWithPendingForPdaWeightDisplayRecursively(d.getChildren(), planWeightMetrics); + overwriteTotalWithPendingForPdaWeightDisplayRecursively(d.getChildren(), planWeightMetrics, nextPlanParent); continue; } @@ -593,17 +1072,30 @@ public class StockShelfOrderApplicationService { BigDecimal alreadyVol = d.getAlreadyShelvesVolume() != null ? d.getAlreadyShelvesVolume() : BigDecimal.ZERO; BigDecimal alreadyArea = d.getAlreadyShelvesArea() != null ? d.getAlreadyShelvesArea() : BigDecimal.ZERO; + // level=2 子行:与收货一致,用计划主行汇总后的累计已上架(flatten 后主行已为主+子合计) + if (planParent != null && d.getLevel() != null && d.getLevel() == 2) { + alreadyNet = planParent.getAlreadyShelvesNetWeight() != null ? planParent.getAlreadyShelvesNetWeight() : BigDecimal.ZERO; + alreadyGross = planParent.getAlreadyShelvesGrossWeight() != null ? planParent.getAlreadyShelvesGrossWeight() : BigDecimal.ZERO; + alreadyVol = planParent.getAlreadyShelvesVolume() != null ? planParent.getAlreadyShelvesVolume() : BigDecimal.ZERO; + alreadyArea = planParent.getAlreadyShelvesArea() != null ? planParent.getAlreadyShelvesArea() : BigDecimal.ZERO; + } + // 参考收货:只对「有计划值」的字段覆盖 total*,否则保留原 total* if (planNet == null && planGross == null && planVol == null && planArea == null) { - overwriteTotalWithPendingForPdaWeightDisplayRecursively(d.getChildren(), planWeightMetrics); + overwriteTotalWithPendingForPdaWeightDisplayRecursively(d.getChildren(), planWeightMetrics, nextPlanParent); continue; } // 兜底:待上架(1/2) 且已上架数量为 0 时, // DB 里的 total_* 可能被初始化为“计划值”,此时不应把它当作 already。 // 按“数量为准”把 already 权重当 0,使 pending 展示为计划值。 + // level=2 时与收货一致:用主行累计数量判断是否已有上架实绩。 BigDecimal alreadyShelvesQty = d.getAlreadyShelvesQuantity() != null ? d.getAlreadyShelvesQuantity() : BigDecimal.ZERO; boolean hasAlreadyQty = alreadyShelvesQty.compareTo(BigDecimal.ZERO) > 0; + if (planParent != null && d.getLevel() != null && d.getLevel() == 2) { + BigDecimal pq = planParent.getAlreadyShelvesQuantity() != null ? planParent.getAlreadyShelvesQuantity() : BigDecimal.ZERO; + hasAlreadyQty = pq.compareTo(BigDecimal.ZERO) > 0; + } if (!hasAlreadyQty) { alreadyNet = BigDecimal.ZERO; alreadyGross = BigDecimal.ZERO; @@ -624,7 +1116,7 @@ public class StockShelfOrderApplicationService { d.setTotalArea(calcPendingMetric(planArea, alreadyArea)); } - overwriteTotalWithPendingForPdaWeightDisplayRecursively(d.getChildren(), planWeightMetrics); + overwriteTotalWithPendingForPdaWeightDisplayRecursively(d.getChildren(), planWeightMetrics, nextPlanParent); } } @@ -743,6 +1235,15 @@ public class StockShelfOrderApplicationService { public Boolean upShelfByApp(StockShelfOrderDO stockReceiptOrderDO){ // 标记 PDA:权重/数量都按“累计”语义处理(与收货保持一致) stockReceiptOrderDO.setFromApp(Boolean.TRUE); + StockShelfOrderPO dbOrder = stockShelfOrderDomainService.getInfo(stockReceiptOrderDO.getShelfOrderId()); + if (dbOrder == null) { + throw new ServiceException("上架单不存在"); + } + ShelfMaterialDetailDO queryAll = new ShelfMaterialDetailDO(); + queryAll.setShelfOrderNumber(dbOrder.getShelfOrderNumber()); + List dbListAll = shelfMaterialDetailDomainService.queryList(queryAll); + preserveParentShelfLineHierarchyFromDb(stockReceiptOrderDO, dbListAll); + List shelfMaterialDetailDOList = stockReceiptOrderDO.getMaterialDetailList(); boolean firstItemHadChildrenBefore = !CollectionUtils.isEmpty(shelfMaterialDetailDOList) && !CollectionUtils.isEmpty(shelfMaterialDetailDOList.get(0).getChildren()); @@ -764,7 +1265,7 @@ public class StockShelfOrderApplicationService { shelfMaterialDetailDO.setStorageSectionId(firstChild.getStorageSectionId()); shelfMaterialDetailDO.setStorageLocationId(firstChild.getStorageLocationId()); shelfMaterialDetailDO.setContainerId(firstChild.getContainerId()); - shelfMaterialDetailDO.setLevel(firstChild.getLevel()); + // 不把子行 level 复制到主项;assembleParamByShelf 会以库表主行 level 为准 shelfMaterialDetailDO.setMaterialDetailSerialNumberList(firstChild.getMaterialDetailSerialNumberList()); // 保留其余子项(从索引1开始),统一设为 level 2 List restChildren = new ArrayList<>(); @@ -777,6 +1278,30 @@ public class StockShelfOrderApplicationService { } }); } + // 与收货 receivingGoodsByApp 一致:assign 后按库表剥离 UI 镜像/已落库展示子行,必要时合成新的 level=2 承接本次量,避免多次提交仍写主行(对齐 injectPdaFlatReceiptAsSyntheticChild) + boolean skipInjectAfterAssign = firstItemHadChildrenBefore + && !CollectionUtils.isEmpty(stockReceiptOrderDO.getMaterialDetailList()) + && !CollectionUtils.isEmpty(stockReceiptOrderDO.getMaterialDetailList().get(0).getChildren()); + Map dbByIdForInject = dbListAll.stream() + .filter(x -> x.getMaterialDetailId() != null) + .collect(Collectors.toMap(ShelfMaterialDetailPO::getMaterialDetailId, Function.identity(), (a, b) -> a)); + Map dbByUidForInject = dbListAll.stream() + .filter(x -> x.getUniqueId() != null) + .collect(Collectors.toMap(ShelfMaterialDetailPO::getUniqueId, Function.identity(), (a, b) -> a)); + int injectIdx = 0; + for (ShelfMaterialDetailDO line : stockReceiptOrderDO.getMaterialDetailList()) { + ShelfMaterialDetailPO dbLine = null; + if (line.getMaterialDetailId() != null) { + dbLine = dbByIdForInject.get(line.getMaterialDetailId()); + } + if (dbLine == null && line.getUniqueId() != null) { + dbLine = dbByUidForInject.get(line.getUniqueId()); + } + if (dbLine != null && !(skipInjectAfterAssign && injectIdx == 0)) { + injectPdaFlatShelfAsSyntheticChild(line, dbLine, dbListAll); + } + injectIdx++; + } //上架装配参数 StockShelfOrderDO assembleStockShelfOrderDO = assembleParamByShelf(stockReceiptOrderDO); assembleStockShelfOrderDO.setFromApp(Boolean.TRUE); @@ -1019,6 +1544,29 @@ public class StockShelfOrderApplicationService { if (shelfMaterialDetailPO == null){ throw new ServiceException("物料明细不存在"); } + // PDA 注入虚拟子行时:净重毛重体积面积只落在子行,主计划行不再把本次增量再累加一遍。 + // upShelfByApp 中 injectPdaFlatShelfAsSyntheticChild 会先写入 children 再进入本方法,需用标志位与下面「无 children」虚拟子行等价处理。 + boolean injectedVirtualShelfChild = Boolean.TRUE.equals(shelfMaterialDetailDO.getPdaShelfSyntheticChildInjected()); + // PDA 常只传主行 materialDetailId、本次数量在 shelvesQuantity,不带 children:第二次起会走主行 UPDATE。 + // 与带子项时一致:计划主行(level=1)且已有上架实绩时,用虚拟子项承接本次量,主行本次数量置 0,子循环走 INSERT 新行。 + if (!injectedVirtualShelfChild && fromApp && CollectionUtils.isEmpty(shelfMaterialDetailDO.getChildren())) { + BigDecimal parentSqRaw = shelfMaterialDetailDO.getShelvesQuantity() == null ? BigDecimal.ZERO : shelfMaterialDetailDO.getShelvesQuantity(); + BigDecimal dbAlreadyMain = shelfMaterialDetailPO.getAlreadyShelvesQuantity() != null + ? shelfMaterialDetailPO.getAlreadyShelvesQuantity() : BigDecimal.ZERO; + if (shelfMaterialDetailPO.getLevel() != null && shelfMaterialDetailPO.getLevel() == 1 + && dbAlreadyMain.compareTo(BigDecimal.ZERO) > 0 + && parentSqRaw.compareTo(BigDecimal.ZERO) > 0) { + ShelfMaterialDetailDO virtualChild = new ShelfMaterialDetailDO(); + BeanUtils.copyProperties(shelfMaterialDetailDO, virtualChild); + virtualChild.setMaterialDetailId(null); + virtualChild.setUniqueId(null); + shelfMaterialDetailDO.setChildren(Collections.singletonList(virtualChild)); + shelfMaterialDetailDO.setShelvesQuantity(BigDecimal.ZERO); + injectedVirtualShelfChild = true; + // 请求主行仍带本次扫描库位;末尾 copy 到 return 时会覆盖库表主行。子行已承载新库位,主行保留库表原库位。 + restorePlanLineStorageFromDb(shelfMaterialDetailDO, shelfMaterialDetailPO); + } + } List children = shelfMaterialDetailDO.getChildren(); BigDecimal childrenShelvesQuantitySum = BigDecimal.ZERO; if (!CollectionUtils.isEmpty(children)) { @@ -1033,6 +1581,15 @@ public class StockShelfOrderApplicationService { //未进行上架直接跳过 continue; } + // PDA:本次上架量只在子行、主行 shelvesQuantity=0 时,upShelfByApp 会把「第一个子行」的仓库/库位回填到主行 DO(见首子赋值逻辑),assemble 末尾再 merge 到 return,会把本次库位写回库表主行覆盖原库位。 + // 与虚拟子行分支里的 restorePlanLineStorageFromDb 一致:主行恢复为库表值;库表主行尚无库位时不恢复,以免清空后无法走下方 setStorageLocationInfo。 + if (fromApp && !CollectionUtils.isEmpty(shelfMaterialDetailDO.getChildren()) + && shelvesQuantity.compareTo(BigDecimal.ZERO) == 0) { + Long dbMainLocId = shelfMaterialDetailPO.getStorageLocationId(); + if (dbMainLocId != null && dbMainLocId != 0L) { + restorePlanLineStorageFromDb(shelfMaterialDetailDO, shelfMaterialDetailPO); + } + } BigDecimal mainItemActualQuantity = shelvesQuantity;//主项本次上架数(不含子项) BigDecimal childrenActualQuantity = BigDecimal.ZERO;//子项本次上架总数 // PDA 权重/体积/面积:记录“本次增量” @@ -1079,6 +1636,19 @@ public class StockShelfOrderApplicationService { } } BigDecimal childShelvesQty = shelfMaterialDetailDOChild.getShelvesQuantity() != null ? shelfMaterialDetailDOChild.getShelvesQuantity() : BigDecimal.ZERO; + // PDA:计划主行(level=1)或已落库的子行(level=2)上已有上架实绩时,再次携带同一 materialDetailId 提交应与「收货」分拆一致:INSERT 新子行(level=2),不在同一行上累加覆盖;否则会 UPDATE 旧子行导致“第二次上架盖住第一次”。 + if (fromApp && shelfMaterialDetailPOChildDb != null) { + Integer dbPlanLevel = shelfMaterialDetailPOChildDb.getLevel(); + BigDecimal dbAlreadyShelved = shelfMaterialDetailPOChildDb.getAlreadyShelvesQuantity() != null + ? shelfMaterialDetailPOChildDb.getAlreadyShelvesQuantity() : BigDecimal.ZERO; + boolean lineHasShelvedHistory = Integer.valueOf(1).equals(dbPlanLevel) + || Integer.valueOf(2).equals(dbPlanLevel); + if (lineHasShelvedHistory + && dbAlreadyShelved.compareTo(BigDecimal.ZERO) > 0 + && childShelvesQty.compareTo(BigDecimal.ZERO) > 0) { + shelfMaterialDetailPOChildDb = null; + } + } if (fromApp) { // 子项未上架数量时,不累加重量增量 BigDecimal childNetInc = childShelvesQty.compareTo(BigDecimal.ZERO) > 0 @@ -1180,7 +1750,9 @@ public class StockShelfOrderApplicationService { } setStorageLocationInfo(returnShelfMaterialDetailDOChild); returnShelfMaterialDetailDOChild.setAllowModify(2); - returnShelfMaterialDetailDOChild.setLevel(2); + // 库表已存在的行:保留其 level(计划主行=1、分批子行=2)。不可一律写 2,否则 PDA 把计划行 id 放在 children 里时会误把主行改成子行。 + Integer dbChildLevel = shelfMaterialDetailPOChildDb.getLevel(); + returnShelfMaterialDetailDOChild.setLevel(dbChildLevel != null ? dbChildLevel : 2); returnShelfMaterialDetailDOChild.setParentUniqueId(shelfMaterialDetailPO.getUniqueId()); // 子项不维护独立计划数量,计划量仅由主项承载 returnShelfMaterialDetailDOChild.setQuantity(null); @@ -1192,6 +1764,8 @@ public class StockShelfOrderApplicationService { BeanUtils.copyProperties(shelfMaterialDetailPO, returnShelfMaterialDetailDOChild, "materialDetailId","createBy","createByName","createTime", "updateBy","updateByName","updateTime"); BeanUtils.copyProperties(shelfMaterialDetailDOChild, returnShelfMaterialDetailDOChild, IgnoreNullUtil.getNullPropertyNames(shelfMaterialDetailDOChild)); + // 新子行必须 INSERT;请求里若仍带上一笔子行的 materialDetailId,上面会把本段误判为 UPDATE + returnShelfMaterialDetailDOChild.setMaterialDetailId(null); if (fromApp) { // 子项不存在:new already = 0 + 本次增量(并不超过计划) WeightMetrics childPlan = shelfMaterialDetailDOChild.getInUniqueId() != null @@ -1253,6 +1827,30 @@ public class StockShelfOrderApplicationService { childrenActualQuantity = childrenActualQuantity.add(returnShelfMaterialDetailDOChild.getShelvesQuantity()); } } + // PDA 分批上架:子行已承载本次量时,主行平面 shelvesQuantity 不应再计入主项累计(与 inject 成功后主行=0 一致)。 + // inject 因 UI 仍带 children 等提前 return 时,主行与子行会双计,此处兜底。 + boolean pdaSplitChildCarriesThisShelf = false; + if (fromApp && shouldPdaSplitIntoShelfChildRow(shelfMaterialDetailPO, shelfMaterialDetailPODbList) + && !CollectionUtils.isEmpty(shelfMaterialDetailDOChildList)) { + boolean anyChildPositiveQty = false; + for (ShelfMaterialDetailDO ch : shelfMaterialDetailDOChildList) { + if (ch != null && ch.getShelvesQuantity() != null + && ch.getShelvesQuantity().compareTo(BigDecimal.ZERO) > 0) { + anyChildPositiveQty = true; + break; + } + } + if (anyChildPositiveQty && shelfMaterialDetailPO.getLevel() != null + && shelfMaterialDetailPO.getLevel() == 1) { + mainItemActualQuantity = BigDecimal.ZERO; + pdaSplitChildCarriesThisShelf = true; + // inject 未把主行 shelvesQuantity 置 0 时,上面「shelvesQuantity==0 才 restore」分支不执行,主行 DO 仍带本次子行库位,merge 会覆盖库表主项库位;与子行兜底双计一致,此处恢复计划主行库位。 + Long dbMainLocId = shelfMaterialDetailPO.getStorageLocationId(); + if (dbMainLocId != null && dbMainLocId != 0L) { + restorePlanLineStorageFromDb(shelfMaterialDetailDO, shelfMaterialDetailPO); + } + } + } BigDecimal dbParentAlready = shelfMaterialDetailPO.getAlreadyShelvesQuantity() != null ? shelfMaterialDetailPO.getAlreadyShelvesQuantity() : BigDecimal.ZERO; // PC:already_shelves_quantity 使用本次上架量;PDA 保持累计口径 @@ -1280,10 +1878,22 @@ public class StockShelfOrderApplicationService { ? shelfMaterialDetailPO.getTotalArea() : BigDecimal.ZERO; - BigDecimal newAlreadyNet = oldAlreadyNet.add(netIncrement); - BigDecimal newAlreadyGross = oldAlreadyGross.add(grossIncrement); - BigDecimal newAlreadyVol = oldAlreadyVol.add(volIncrement); - BigDecimal newAlreadyArea = oldAlreadyArea.add(areaIncrement); + BigDecimal newAlreadyNet; + BigDecimal newAlreadyGross; + BigDecimal newAlreadyVol; + BigDecimal newAlreadyArea; + if (injectedVirtualShelfChild || pdaSplitChildCarriesThisShelf) { + // 与分拆子行一致:本次 total_* 增量只落在子行;主行保持库表累计不变(否则 netIncrement 已由子循环写入子行,父行再 old+net 会双计)。 + newAlreadyNet = oldAlreadyNet; + newAlreadyGross = oldAlreadyGross; + newAlreadyVol = oldAlreadyVol; + newAlreadyArea = oldAlreadyArea; + } else { + newAlreadyNet = oldAlreadyNet.add(netIncrement); + newAlreadyGross = oldAlreadyGross.add(grossIncrement); + newAlreadyVol = oldAlreadyVol.add(volIncrement); + newAlreadyArea = oldAlreadyArea.add(areaIncrement); + } WeightMetrics plan = shelfMaterialDetailPO.getInUniqueId() != null ? planWeightMetrics.get(shelfMaterialDetailPO.getReceiptUniqueId() != null @@ -1334,6 +1944,8 @@ public class StockShelfOrderApplicationService { ShelfMaterialDetailDO returnShelfMaterialDetailDO = new ShelfMaterialDetailDO(); BeanUtils.copyProperties(shelfMaterialDetailPO, returnShelfMaterialDetailDO); BeanUtils.copyProperties(shelfMaterialDetailDO, returnShelfMaterialDetailDO, IgnoreNullUtil.getNullPropertyNames(shelfMaterialDetailDO)); + // 主行 level 以库表为准,避免 upShelfByApp 等路径把 firstChild.level 合到主项后覆盖为 2 + returnShelfMaterialDetailDO.setLevel(shelfMaterialDetailPO.getLevel()); if (shelfMaterialDetailPO.getShelvesStatus() == 1){ //首次上架 //是否开启了序列号管理 @@ -1355,6 +1967,109 @@ public class StockShelfOrderApplicationService { return returnStockShelfOrderDO; } + /** + * 将计划主行上的仓库/库区/库位/容器恢复为库表现值,避免 PDA 二次上架时把「本次子行库位」写回主行。 + */ + private void restorePlanLineStorageFromDb(ShelfMaterialDetailDO parentDo, ShelfMaterialDetailPO dbPo) { + if (parentDo == null || dbPo == null) { + return; + } + parentDo.setWarehouseId(dbPo.getWarehouseId()); + parentDo.setWarehouseCode(dbPo.getWarehouseCode()); + parentDo.setWarehouseName(dbPo.getWarehouseName()); + parentDo.setStorageSectionId(dbPo.getStorageSectionId()); + parentDo.setStorageCode(dbPo.getStorageCode()); + parentDo.setStorageName(dbPo.getStorageName()); + parentDo.setStorageLocationId(dbPo.getStorageLocationId()); + parentDo.setStorageLocationCode(dbPo.getStorageLocationCode()); + parentDo.setStorageLocationName(dbPo.getStorageLocationName()); + parentDo.setContainerId(dbPo.getContainerId()); + parentDo.setContainerCode(dbPo.getContainerCode()); + parentDo.setContainerType(dbPo.getContainerType()); + parentDo.setContainerTypeName(dbPo.getContainerTypeName()); + } + + /** + * 请求行与库主计划行 level 不一致时纠正,避免误当子行(对齐收货 preserveParentReceiptLineHierarchyFromDb)。 + */ + private void preserveParentShelfLineHierarchyFromDb(StockShelfOrderDO order, List dbList) { + if (order == null || CollectionUtils.isEmpty(order.getMaterialDetailList()) || CollectionUtils.isEmpty(dbList)) { + return; + } + Map byId = dbList.stream() + .filter(x -> x.getMaterialDetailId() != null) + .collect(Collectors.toMap(ShelfMaterialDetailPO::getMaterialDetailId, Function.identity(), (a, b) -> a)); + Map byUid = dbList.stream() + .filter(x -> x.getUniqueId() != null) + .collect(Collectors.toMap(ShelfMaterialDetailPO::getUniqueId, Function.identity(), (a, b) -> a)); + for (ShelfMaterialDetailDO line : order.getMaterialDetailList()) { + ShelfMaterialDetailPO db = null; + if (line.getMaterialDetailId() != null) { + db = byId.get(line.getMaterialDetailId()); + } + if (db == null && line.getUniqueId() != null) { + db = byUid.get(line.getUniqueId()); + } + if (db != null && db.getLevel() != null && db.getLevel() == 1 + && line.getLevel() != null && line.getLevel() == 2) { + line.setLevel(1); + line.setParentUniqueId(null); + } + } + } + + /** + * PDA 单次传主行平面(无真实子结构需拆时):与收货 {@code injectPdaFlatReceiptAsSyntheticChild} 对齐—— + * 去掉 UI 镜像行与已落库展示子行后,将本次上架数量与 total* 指标挪到新建 level=2 子行,主行清零并恢复库表库位,避免第 2、3 次仍累加到主项、覆盖主行库位。 + */ + private void injectPdaFlatShelfAsSyntheticChild(ShelfMaterialDetailDO line, ShelfMaterialDetailPO dbLine, + List allDbLines) { + if (line == null || dbLine == null) { + return; + } + stripPdaUiParentCopyChildrenShelf(line, dbLine); + stripPdaExistingDisplayedChildrenForResplitShelf(line, dbLine, allDbLines); + if (dbLine.getLevel() != null && dbLine.getLevel() == 2 && !isCorruptedStandaloneLineAsLevel2Shelf(dbLine)) { + return; + } + if (!CollectionUtils.isEmpty(line.getChildren())) { + return; + } + if (!shouldPdaSplitIntoShelfChildRow(dbLine, allDbLines) && !isCorruptedStandaloneLineAsLevel2Shelf(dbLine)) { + return; + } + BigDecimal sq = line.getShelvesQuantity(); + boolean hasQty = sq != null && sq.compareTo(BigDecimal.ZERO) > 0; + boolean hasMetricBatch = hasPdaShelfMetricIncrement(line, dbLine); + if (!hasQty && !hasMetricBatch) { + return; + } + ShelfMaterialDetailDO child = new ShelfMaterialDetailDO(); + BeanUtils.copyProperties(line, child, "children", "materialDetailId", "uniqueId", "level", "parentUniqueId"); + child.setUniqueId(idGenerator.snowflakeId(SnowFlakeConstants.wmsId)); + child.setChildren(null); + child.setLevel(2); + child.setParentUniqueId(resolveParentUniqueIdForPdaInjectChildShelf(dbLine, allDbLines)); + if (hasQty) { + child.setShelvesQuantity(sq); + } + child.setTotalNetWeight(line.getTotalNetWeight()); + child.setTotalGrossWeight(line.getTotalGrossWeight()); + child.setTotalVolume(line.getTotalVolume()); + child.setTotalArea(line.getTotalArea()); + child.setAlreadyShelvesQuantity(null); + line.setChildren(Collections.singletonList(child)); + line.setShelvesQuantity(BigDecimal.ZERO); + line.setTotalNetWeight(null); + line.setTotalGrossWeight(null); + line.setTotalVolume(null); + line.setTotalArea(null); + line.setLevel(1); + line.setParentUniqueId(null); + restorePlanLineStorageFromDb(line, dbLine); + line.setPdaShelfSyntheticChildInjected(true); + } + /** * @description 设置上架库位信息 * @author ZhouGY diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/facade/IReceiptMaterialDetailService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/facade/IReceiptMaterialDetailService.java index ba14406e7..815bb3cf9 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/facade/IReceiptMaterialDetailService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/facade/IReceiptMaterialDetailService.java @@ -26,6 +26,11 @@ public interface IReceiptMaterialDetailService extends IService queryListChildren(ReceiptMaterialDetailDO receiptMaterialDetailDO); + /** + * PDA:按收货单号 + 父计划行 material_detail_id,查询其下 level=2 子行;若无子行但主行已有实收,返回主行快照。 + */ + List queryLevel2ChildrenByParentMaterialDetailId(String receiptOrderNumber, Long parentMaterialDetailId); + /** * 新增物料明细 */ diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/persistence/ReceiptMaterialDetailImpl.java b/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/persistence/ReceiptMaterialDetailImpl.java index 2e93f4599..e6ba18b33 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/persistence/ReceiptMaterialDetailImpl.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/receiptMaterialDetail/repository/persistence/ReceiptMaterialDetailImpl.java @@ -50,6 +50,7 @@ import lombok.extern.slf4j.Slf4j; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -146,6 +147,85 @@ public class ReceiptMaterialDetailImpl extends ServiceImpl queryLevel2ChildrenByParentMaterialDetailId(String receiptOrderNumber, + Long parentMaterialDetailId) { + List empty = new ArrayList<>(); + if (receiptOrderNumber == null || parentMaterialDetailId == null) { + return empty; + } + ReceiptMaterialDetailDO q = new ReceiptMaterialDetailDO(); + q.setReceiptOrderNumber(receiptOrderNumber); + List all = receiptMaterialDetailMapper.queryList(q); + if (CollectionUtils.isEmpty(all)) { + return empty; + } + ReceiptMaterialDetailPO anchor = all.stream() + .filter(p -> parentMaterialDetailId.equals(p.getMaterialDetailId())) + .findFirst() + .orElse(null); + if (anchor == null) { + return empty; + } + ReceiptMaterialDetailPO parent = anchor; + if (anchor.getLevel() != null && anchor.getLevel() == 2 && anchor.getParentUniqueId() != null) { + Long puid = anchor.getParentUniqueId(); + ReceiptMaterialDetailPO plan = all.stream() + .filter(row -> puid.equals(row.getUniqueId()) || puid.equals(row.getInUniqueId())) + .findFirst() + .orElse(null); + if (plan != null) { + parent = plan; + } + } + Long pid = parent.getUniqueId(); + Long inPid = parent.getInUniqueId(); + if (pid == null && inPid == null) { + return empty; + } + final Long fpid = pid; + final Long finPid = inPid; + List children = all.stream() + .filter(m -> m.getLevel() != null && m.getLevel() == 2) + .filter(m -> { + Long p = m.getParentUniqueId(); + if (p == null) { + return false; + } + return (fpid != null && p.equals(fpid)) || (finPid != null && p.equals(finPid)); + }) + .sorted(Comparator.comparing(ReceiptMaterialDetailPO::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(ReceiptMaterialDetailPO::getMaterialDetailId, Comparator.nullsLast(Comparator.naturalOrder()))) + .collect(Collectors.toList()); + children.forEach(ch -> { + List materialMoreDetailList = buildMaterialMoreDetailListByMaterialBinding(ch); + fillAttributeValueFromInMaterialDetail(ch, materialMoreDetailList); + ch.setMaterialMoreDetailList(materialMoreDetailList); + }); + // 首笔只落主计划行、后续批次才 INSERT 子行时:主行上仍有累计实收,列表需在子行之外再带一条「主行首笔」快照 + if (!children.isEmpty() && parent.getLevel() != null && parent.getLevel() == 1 + && parent.getAlreadyReceiptQuantity() != null + && parent.getAlreadyReceiptQuantity().compareTo(BigDecimal.ZERO) > 0) { + ReceiptMaterialDetailPO mainSnap = new ReceiptMaterialDetailPO(); + BeanUtils.copyProperties(parent, mainSnap); + List mainMore = buildMaterialMoreDetailListByMaterialBinding(mainSnap); + fillAttributeValueFromInMaterialDetail(mainSnap, mainMore); + mainSnap.setMaterialMoreDetailList(mainMore); + children.add(0, mainSnap); + } + // 未拆 level=2 子行、实收全部记在主计划行时:仍返回一条与主行一致的快照,供 PDA「已收货列表」展示 + if (children.isEmpty() && parent.getAlreadyReceiptQuantity() != null + && parent.getAlreadyReceiptQuantity().compareTo(BigDecimal.ZERO) > 0) { + ReceiptMaterialDetailPO mainSnap = new ReceiptMaterialDetailPO(); + BeanUtils.copyProperties(parent, mainSnap); + List materialMoreDetailList = buildMaterialMoreDetailListByMaterialBinding(mainSnap); + fillAttributeValueFromInMaterialDetail(mainSnap, materialMoreDetailList); + mainSnap.setMaterialMoreDetailList(materialMoreDetailList); + children.add(mainSnap); + } + return children; + } + private void collectUniqueIdsRecursively(ReceiptMaterialDetailPO po, java.util.Set set) { if (po == null || po.getUniqueId() == null) return; set.add(po.getUniqueId()); diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/facade/IShelfMaterialDetailService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/facade/IShelfMaterialDetailService.java index a2598f51e..4ecf44072 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/facade/IShelfMaterialDetailService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/facade/IShelfMaterialDetailService.java @@ -26,6 +26,11 @@ public interface IShelfMaterialDetailService extends IService queryListChildren(ShelfMaterialDetailDO shelfMaterialDetailDO); + /** + * PDA:指定计划行(卡片 material_detail_id)下所有 level=2 分批上架子行;可含首笔仅落主行时的主行快照。 + */ + public List queryLevel2ChildrenByParentMaterialDetailId(String shelfOrderNumber, Long parentMaterialDetailId); + /** * 新增物料明细 */ diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/persistence/ShelfMaterialDetailImpl.java b/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/persistence/ShelfMaterialDetailImpl.java index 15a19c181..35aa716e3 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/persistence/ShelfMaterialDetailImpl.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/shelfMaterialDetail/repository/persistence/ShelfMaterialDetailImpl.java @@ -31,8 +31,10 @@ import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -123,6 +125,103 @@ public class ShelfMaterialDetailImpl extends ServiceImpl queryLevel2ChildrenByParentMaterialDetailId(String shelfOrderNumber, + Long parentMaterialDetailId) { + List empty = new ArrayList<>(); + if (shelfOrderNumber == null || parentMaterialDetailId == null) { + return empty; + } + ShelfMaterialDetailDO q = new ShelfMaterialDetailDO(); + q.setShelfOrderNumber(shelfOrderNumber); + List all = shelfMaterialDetailMapper.queryList(q); + if (CollectionUtils.isEmpty(all)) { + return empty; + } + ShelfMaterialDetailPO anchor = all.stream() + .filter(p -> parentMaterialDetailId.equals(p.getMaterialDetailId())) + .findFirst() + .orElse(null); + if (anchor == null) { + return empty; + } + ShelfMaterialDetailPO parent = anchor; + if (anchor.getLevel() != null && anchor.getLevel() == 2 && anchor.getParentUniqueId() != null) { + Long puid = anchor.getParentUniqueId(); + ShelfMaterialDetailPO plan = all.stream() + .filter(row -> puid.equals(row.getUniqueId()) || puid.equals(row.getInUniqueId())) + .findFirst() + .orElse(null); + if (plan != null) { + parent = plan; + } + } + Long pid = parent.getUniqueId(); + Long inPid = parent.getInUniqueId(); + if (pid == null && inPid == null) { + return empty; + } + final Long fpid = pid; + final Long finPid = inPid; + List children = all.stream() + .filter(m -> m.getLevel() != null && m.getLevel() == 2) + .filter(m -> { + Long p = m.getParentUniqueId(); + if (p == null) { + return false; + } + return (fpid != null && p.equals(fpid)) || (finPid != null && p.equals(finPid)); + }) + .sorted(Comparator.comparing(ShelfMaterialDetailPO::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(ShelfMaterialDetailPO::getMaterialDetailId, Comparator.nullsLast(Comparator.naturalOrder()))) + .collect(Collectors.toList()); + children.forEach(this::enrichShelfRowMaterialMoreDetail); + // 首笔只落主计划行、后续批次才 INSERT 子行时:主行上仍有累计已上架,列表需在子行之外再带一条「主行首笔」快照 + if (!children.isEmpty() && parent.getLevel() != null && parent.getLevel() == 1 + && parent.getAlreadyShelvesQuantity() != null + && parent.getAlreadyShelvesQuantity().compareTo(BigDecimal.ZERO) > 0) { + ShelfMaterialDetailPO mainSnap = new ShelfMaterialDetailPO(); + BeanUtils.copyProperties(parent, mainSnap); + enrichShelfRowMaterialMoreDetail(mainSnap); + children.add(0, mainSnap); + } + // 未拆 level=2 子行、已上架全部记在主计划行时:仍返回一条与主行一致的快照,供 PDA「历史上架列表」展示 + if (children.isEmpty() && parent.getAlreadyShelvesQuantity() != null + && parent.getAlreadyShelvesQuantity().compareTo(BigDecimal.ZERO) > 0) { + ShelfMaterialDetailPO mainSnap = new ShelfMaterialDetailPO(); + BeanUtils.copyProperties(parent, mainSnap); + enrichShelfRowMaterialMoreDetail(mainSnap); + children.add(mainSnap); + } + return children; + } + + /** + * 与 queryListChildren 中单行一致:批次更多属性 + 从上架明细填充 attributeValue。 + */ + private void enrichShelfRowMaterialMoreDetail(ShelfMaterialDetailPO row) { + if (row == null) { + return; + } + MaterialMoreDetailDO materialMoreDetailDO = new MaterialMoreDetailDO(); + Long materialDetailUniqueIdForQuery = row.getReceiptUniqueId() != null + ? row.getReceiptUniqueId() : row.getUniqueId(); + materialMoreDetailDO.setMaterialDetailUniqueId(materialDetailUniqueIdForQuery); + List materialMoreDetailList = IMaterialMoreDetailService.queryList(materialMoreDetailDO); + if (CollectionUtils.isEmpty(materialMoreDetailList)) { + AjaxResult ajaxResult = systemServiceFeign.getBatchDetailListByMaterialBaseInfoId(row.getMaterialBaseInfoId()); + if (ajaxResult != null && "200".equals(String.valueOf(ajaxResult.get("code")))) { + List batchList = JSON.parseArray(JSONObject.toJSONString(ajaxResult.get("data")), BatchDetailFeignPO.class); + materialMoreDetailList = filterAndConvertToMaterialMoreDetail(batchList); + } + } + if (materialMoreDetailList == null) { + materialMoreDetailList = new ArrayList<>(); + } + fillAttributeValueFromShelfMaterialDetail(row, materialMoreDetailList); + row.setMaterialMoreDetailList(materialMoreDetailList); + } + /** * 过滤 isDisplay=1 的批次属性并转换为 MaterialMoreDetailPO,与收货/PC 端一致 */ @@ -309,9 +408,13 @@ public class ShelfMaterialDetailImpl extends ServiceImpl shelfMaterialDetailChildList = new ArrayList<>(); if (!CollectionUtils.isEmpty(inMaterialDetailDOChildList)){ for (ShelfMaterialDetailDO shelfMaterialDetailDOChild : inMaterialDetailDOChildList){ + // 与收货 batchReceiptUpdate 一致:不强行 setLevel(2)。PDA 可能在 children 中带 parent_copy(与主行同 material_detail_id/unique_id), + // 若再强制 level=2,saveOrUpdateBatch 后写会覆盖主行,把主行改成 level=2。 + if (isShelfParentCopyMirrorRow(shelfMaterialDetailDO, shelfMaterialDetailDOChild)) { + continue; + } ShelfMaterialDetail shelfMaterialDetailChild = new ShelfMaterialDetail(); BeanUtils.copyProperties(shelfMaterialDetailDOChild, shelfMaterialDetailChild); - shelfMaterialDetailChild.setLevel(2); shelfMaterialDetailChild.setCreateBy(loginUser.getUserid()); shelfMaterialDetailChild.setCreateByName(loginUser.getUsername()); shelfMaterialDetailChild.setCreateTime(new Date()); @@ -359,6 +462,33 @@ public class ShelfMaterialDetailImpl extends ServiceImpl listLevel2ChildrenByParentForApp(Long receiptOrderId, Long parentMaterialDetailId) { + StockReceiptOrderPO order = stockReceiptOrderService.getInfo(receiptOrderId); + if (order == null) { + throw new ServiceException("收货单不存在"); + } + List children = receiptMaterialDetailService.queryLevel2ChildrenByParentMaterialDetailId( + order.getReceiptOrderNumber(), parentMaterialDetailId); + if (CollectionUtils.isEmpty(children)) { + return new ArrayList<>(); + } + InMaterialDetailSerialNumberDO snDo = new InMaterialDetailSerialNumberDO(); + snDo.setOrderNumber(order.getReceiptOrderNumber()); + List snList = inMaterialDetailSerialNumberService.queryList(snDo); + Map> snMap = snList.stream() + .collect(Collectors.groupingBy(InMaterialDetailSerialNumberPO::getMaterialDetailUniqueId)); + for (ReceiptMaterialDetailPO ch : children) { + List sns = ch.getUniqueId() != null ? snMap.get(ch.getUniqueId()) : null; + ch.setMaterialDetailSerialNumberList(!CollectionUtils.isEmpty(sns) ? sns : new ArrayList<>()); + } + return children; + } + /** * 根据物料绑定的批次属性动态获取更多属性(按 materialBaseInfoId,不再使用 MaterialGoodsRule.batchId) * @param receiptMaterialDetailPO 收货明细 @@ -640,6 +668,91 @@ public class StockReceiptOrderDomainService { * @author ZhouGY * @date 2024/6/14 17:27 */ + /** + * 整单完成判断:只认计划主行(level 空或 1)。分批子行(level=2)不应单独占一票,否则易与主行同组已收尾仍计为「未完结」。 + */ + private static boolean isReceiptPlanLineForOrderCompletionGate(ReceiptMaterialDetailDO d) { + Integer lv = d.getLevel(); + return lv == null || lv == 1; + } + + private static boolean sameLongIdForReceiptGate(Long a, Long b) { + if (a == null || b == null) { + return false; + } + return a.longValue() == b.longValue(); + } + + /** + * 是否与 {@link com.mhd.wms.application.service.stockReceiptOrder.StockReceiptOrderApplicationService} 中 + * PDA flatten / 超收校验用的「计划父行下 level=2」范围一致。 + */ + private static boolean isLevel2ChildUnderPlanForReceiptCompletionGate(ReceiptMaterialDetailPO plan, ReceiptMaterialDetailPO x) { + if (plan == null || x == null) { + return false; + } + if (x.getLevel() == null || x.getLevel() != 2) { + return false; + } + Long puid = x.getParentUniqueId(); + if (puid == null || Long.valueOf(0).equals(puid)) { + return false; + } + Long uid = plan.getUniqueId(); + Long inUid = plan.getInUniqueId(); + boolean linked = (uid != null && sameLongIdForReceiptGate(uid, puid)) + || (inUid != null && sameLongIdForReceiptGate(inUid, puid)); + if (!linked) { + return false; + } + return plan.getUniqueId() == null || !sameLongIdForReceiptGate(plan.getUniqueId(), x.getUniqueId()); + } + + private static BigDecimal nzBdGate(BigDecimal v) { + return v != null ? v : BigDecimal.ZERO; + } + + /** 计划父行自身 already_receipt + 其下归属子行,与 PDA 汇总口径一致 */ + private static BigDecimal sumAlreadyReceiptUnderPlanForWholeOrderGate(ReceiptMaterialDetailPO plan, + List allRows) { + if (plan == null || CollectionUtils.isEmpty(allRows)) { + return BigDecimal.ZERO; + } + BigDecimal sum = nzBdGate(plan.getAlreadyReceiptQuantity()); + for (ReceiptMaterialDetailPO x : allRows) { + if (isLevel2ChildUnderPlanForReceiptCompletionGate(plan, x)) { + sum = sum.add(nzBdGate(x.getAlreadyReceiptQuantity())); + } + } + return sum; + } + + private static boolean isReceiptPlanLinePoForGate(ReceiptMaterialDetailPO p) { + if (p == null) { + return false; + } + Integer lv = p.getLevel(); + return lv == null || lv == 1; + } + + /** + * 计划主行是否仍阻塞整单完成:状态为待收/收货中,且(主+子)累计已收仍低于计划量。 + * 多批次拆子行后主行 receipt_status 可能长期为 2,但累计已收已满,不得再阻塞。 + */ + private static boolean isPlanLineStillBlockingWholeOrderCompletion(ReceiptMaterialDetailPO plan, + List allDbLines) { + Integer st = plan.getReceiptStatus(); + if (st == null || st >= 3) { + return false; + } + BigDecimal planQty = plan.getQuantity(); + if (planQty == null) { + planQty = BigDecimal.ZERO; + } + BigDecimal sumAlready = sumAlreadyReceiptUnderPlanForWholeOrderGate(plan, allDbLines); + return sumAlready.compareTo(planQty) < 0; + } + @Transactional(rollbackFor = Exception.class) public Boolean updateStockReceiptOrder(StockReceiptOrderDO stockReceiptOrderDO, StockReceiptOrderPO stockReceiptOrderPO) { LoginUser loginUser = SecurityUtils.getLoginUser(); @@ -664,8 +777,26 @@ public class StockReceiptOrderDomainService { stockReceiptOrder.setUpdateByName(loginUser.getUsername()); stockReceiptOrder.setUpdateTime(new Date()); stockReceiptOrderService.updateById(stockReceiptOrder); - //获取所有明细中的收货状态小于3的数量--未完成收货的明细 - long receiptMaterialQuantity = stockReceiptOrderDO.getMaterialDetailList().stream().filter(receiptMaterialDetailDO -> receiptMaterialDetailDO.getReceiptStatus() < 3).count(); + // 必须以落库后的全量明细为准:请求体里的主行 receipt_status 在多批次拆子行后可能仍为「收货中」, + // 但主行+子行 already_receipt 已满计划量,不得阻塞 completeReceipt。 + long receiptMaterialQuantity = 0L; + if (StringUtils.isNotBlank(stockReceiptOrderPO.getReceiptOrderNumber())) { + ReceiptMaterialDetailDO qAll = new ReceiptMaterialDetailDO(); + qAll.setReceiptOrderNumber(stockReceiptOrderPO.getReceiptOrderNumber()); + List allDbLines = receiptMaterialDetailService.queryList(qAll); + if (!CollectionUtils.isEmpty(allDbLines)) { + receiptMaterialQuantity = allDbLines.stream() + .filter(StockReceiptOrderDomainService::isReceiptPlanLinePoForGate) + .filter(p -> isPlanLineStillBlockingWholeOrderCompletion(p, allDbLines)) + .count(); + } + } else { + receiptMaterialQuantity = stockReceiptOrderDO.getMaterialDetailList().stream() + .filter(d -> d != null && isReceiptPlanLineForOrderCompletionGate(d)) + .filter(receiptMaterialDetailDO -> receiptMaterialDetailDO.getReceiptStatus() != null + && receiptMaterialDetailDO.getReceiptStatus() < 3) + .count(); + } //收货数量大于等于计划数量 直接完成收货 if (stockReceiptOrder.getReceiptQuantity().compareTo(stockReceiptOrderPO.getQuantity()) >= 0 && receiptMaterialQuantity == 0){ completeReceipt(stockReceiptOrderPO.getReceiptOrderId(), stockReceiptOrderDO.getFromPda()); diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/repository/po/StockShelfOrderPO.java b/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/repository/po/StockShelfOrderPO.java index abe8d130d..4d1b8910c 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/repository/po/StockShelfOrderPO.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/repository/po/StockShelfOrderPO.java @@ -105,4 +105,21 @@ public class StockShelfOrderPO extends StockInOrderBasePO { @ApiModelProperty("物料明细") private List materialDetailList; + /** 详情接口按行递归汇总(与收货单 getInfo 一致),供 PC 展示 */ + @ApiModelProperty("总净重(KG)") + @Excel(name = "总净重(KG)") + private BigDecimal totalNetWeight; + + @ApiModelProperty("总毛重(KG)") + @Excel(name = "总毛重(KG)") + private BigDecimal totalGrossWeight; + + @ApiModelProperty("总体积(CBM)") + @Excel(name = "总体积(CBM)") + private BigDecimal totalVolume; + + @ApiModelProperty("总面积(SQM)") + @Excel(name = "总面积(SQM)") + private BigDecimal totalArea; + } diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/service/StockShelfOrderDomainService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/service/StockShelfOrderDomainService.java index bad4ab18d..6b2f3435c 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/service/StockShelfOrderDomainService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/stockShelfOrder/service/StockShelfOrderDomainService.java @@ -6,6 +6,7 @@ import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.mhd.common.core.constant.SnowFlakeConstants; import com.mhd.common.core.domain.po.UserPo; @@ -257,7 +258,33 @@ public class StockShelfOrderDomainService { return stockShelfOrderPO; } - + /** + * PDA:指定计划行(卡片)下所有 level=2 分批上架子行(库表真实子行,不含 parent_copy),附带序列号。 + * + * @param shelfOrderId 上架单主键 + * @param parentMaterialDetailId 卡片对应 shelf_material_detail.material_detail_id(可误传为某子行 id,将解析到其父计划行) + */ + public List listLevel2ChildrenByParentForApp(Long shelfOrderId, Long parentMaterialDetailId) { + StockShelfOrderPO order = stockShelfOrderService.getInfo(shelfOrderId); + if (order == null) { + throw new ServiceException("上架单不存在"); + } + List children = shelfMaterialDetailService.queryLevel2ChildrenByParentMaterialDetailId( + order.getShelfOrderNumber(), parentMaterialDetailId); + if (CollectionUtils.isEmpty(children)) { + return new ArrayList<>(); + } + InMaterialDetailSerialNumberDO snDo = new InMaterialDetailSerialNumberDO(); + snDo.setOrderNumber(order.getShelfOrderNumber()); + List snList = inMaterialDetailSerialNumberService.queryList(snDo); + Map> snMap = snList.stream() + .collect(Collectors.groupingBy(InMaterialDetailSerialNumberPO::getMaterialDetailUniqueId)); + for (ShelfMaterialDetailPO ch : children) { + List sns = ch.getUniqueId() != null ? snMap.get(ch.getUniqueId()) : null; + ch.setMaterialDetailSerialNumberList(!CollectionUtils.isEmpty(sns) ? sns : new ArrayList<>()); + } + return children; + } /** * @description 任务下发 @@ -313,6 +340,10 @@ public class StockShelfOrderDomainService { genContainerUsageRecord(stockShelfOrderDO); //修改物料明细信息 shelfMaterialDetailService.batchShelfUpdate(stockShelfOrderDO); + LoginUser loginUserUpShelf = SecurityUtils.getLoginUser(); + String userRealNameUpShelf = resolveUserRealNameForShelf(loginUserUpShelf); + refreshShelfMaterialDetailShelvesStatusAfterPartialUpShelf(stockShelfOrderPO.getShelfOrderNumber(), loginUserUpShelf, + userRealNameUpShelf); //同步更新入库单明细的上架数量/上架状态 syncShelvesQuantityToInMaterialDetail(stockShelfOrderDO); //生成 上架作业单 @@ -652,11 +683,225 @@ public class StockShelfOrderDomainService { if (abnormal == 1){ genInOrderAbnormal(stockShelfOrderPO); } + String userRealNameComplete = resolveUserRealNameForShelf(loginUser); + updateShelfMaterialDetailShelvesStatusWhenOrderCompleted(stockShelfOrderPO, loginUser, userRealNameComplete); //推送业务数据到BMS系统 // pushSyncBmsDomainService.pushInShelfOrder(stockShelfOrderPO.getInOrderNumber()); return true; } + /** + * 分批/单次上架提交后:按主行+子行汇总「累计已上架」与主行计划量比较,刷新各明细 shelves_status(待上 1 / 上中 2 / 满 3 / 超 5) + */ + private void refreshShelfMaterialDetailShelvesStatusAfterPartialUpShelf(String shelfOrderNumber, + LoginUser loginUser, String userRealName) { + if (StringUtils.isBlank(shelfOrderNumber) || loginUser == null) { + return; + } + Date updateTime = new Date(); + List details = shelfMaterialDetailService.list( + new QueryWrapper().lambda() + .eq(ShelfMaterialDetail::getShelfOrderNumber, shelfOrderNumber) + .eq(ShelfMaterialDetail::getDelFlag, 1)); + if (CollectionUtils.isEmpty(details)) { + return; + } + Map> childrenByParentUniqueId = new HashMap<>(); + for (ShelfMaterialDetail d : details) { + if (isShelfDetailChildRow(d) && d.getParentUniqueId() != null) { + childrenByParentUniqueId.computeIfAbsent(d.getParentUniqueId(), k -> new ArrayList<>()).add(d); + } + } + Set parentUniqueIdsInOrder = details.stream() + .filter(d -> !isShelfDetailChildRow(d)) + .map(ShelfMaterialDetail::getUniqueId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + for (ShelfMaterialDetail parent : details) { + if (isShelfDetailChildRow(parent)) { + continue; + } + Integer status; + List children = parent.getUniqueId() == null ? null + : childrenByParentUniqueId.get(parent.getUniqueId()); + if (!CollectionUtils.isEmpty(children)) { + BigDecimal groupShelved = nzShelf(parent.getAlreadyShelvesQuantity()); + for (ShelfMaterialDetail ch : children) { + groupShelved = groupShelved.add(nzShelf(ch.getAlreadyShelvesQuantity())); + } + status = deriveShelfDetailStatusWhileOrderOpen(groupShelved, parent.getQuantity()); + } else { + status = deriveShelfDetailStatusWhileOrderOpen(parent.getAlreadyShelvesQuantity(), parent.getQuantity()); + } + parent.setShelvesStatus(status); + parent.setUpdateBy(loginUser.getUserid()); + parent.setUpdateByName(userRealName); + parent.setUpdateTime(updateTime); + if (!CollectionUtils.isEmpty(children)) { + for (ShelfMaterialDetail ch : children) { + ch.setShelvesStatus(status); + ch.setUpdateBy(loginUser.getUserid()); + ch.setUpdateByName(userRealName); + ch.setUpdateTime(updateTime); + } + } + } + for (ShelfMaterialDetail d : details) { + if (isShelfDetailChildRow(d) && d.getParentUniqueId() != null + && !parentUniqueIdsInOrder.contains(d.getParentUniqueId())) { + d.setShelvesStatus(deriveShelfDetailStatusWhileOrderOpen(d.getAlreadyShelvesQuantity(), d.getQuantity())); + d.setUpdateBy(loginUser.getUserid()); + d.setUpdateByName(userRealName); + d.setUpdateTime(updateTime); + } + } + flushShelfMaterialDetailShelvesStatusColumnsOnly(details, loginUser, userRealName); + } + + /** + * 整单完成上架:按累计已上架与计划比较,更新明细 shelves_status(3 已上满 / 4 少上 / 5 超上) + */ + private void updateShelfMaterialDetailShelvesStatusWhenOrderCompleted(StockShelfOrderPO stockShelfOrderPO, + LoginUser loginUser, String userRealName) { + if (stockShelfOrderPO == null || StringUtils.isBlank(stockShelfOrderPO.getShelfOrderNumber()) || loginUser == null) { + return; + } + Date updateTime = new Date(); + List details = shelfMaterialDetailService.list( + new QueryWrapper().lambda() + .eq(ShelfMaterialDetail::getShelfOrderNumber, stockShelfOrderPO.getShelfOrderNumber()) + .eq(ShelfMaterialDetail::getDelFlag, 1)); + if (CollectionUtils.isEmpty(details)) { + return; + } + Map> childrenByParentUniqueId = new HashMap<>(); + for (ShelfMaterialDetail d : details) { + if (isShelfDetailChildRow(d) && d.getParentUniqueId() != null) { + childrenByParentUniqueId.computeIfAbsent(d.getParentUniqueId(), k -> new ArrayList<>()).add(d); + } + } + Set parentUniqueIdsInOrder = details.stream() + .filter(d -> !isShelfDetailChildRow(d)) + .map(ShelfMaterialDetail::getUniqueId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + for (ShelfMaterialDetail parent : details) { + if (isShelfDetailChildRow(parent)) { + continue; + } + Integer status; + List children = parent.getUniqueId() == null ? null + : childrenByParentUniqueId.get(parent.getUniqueId()); + if (!CollectionUtils.isEmpty(children)) { + BigDecimal groupShelved = nzShelf(parent.getAlreadyShelvesQuantity()); + for (ShelfMaterialDetail ch : children) { + groupShelved = groupShelved.add(nzShelf(ch.getAlreadyShelvesQuantity())); + } + status = resolveShelfDetailStatusOnOrderComplete(groupShelved, parent.getQuantity()); + } else { + status = resolveShelfDetailStatusOnOrderComplete(parent.getAlreadyShelvesQuantity(), parent.getQuantity()); + } + parent.setShelvesStatus(status); + parent.setUpdateBy(loginUser.getUserid()); + parent.setUpdateByName(userRealName); + parent.setUpdateTime(updateTime); + if (!CollectionUtils.isEmpty(children)) { + for (ShelfMaterialDetail ch : children) { + ch.setShelvesStatus(status); + ch.setUpdateBy(loginUser.getUserid()); + ch.setUpdateByName(userRealName); + ch.setUpdateTime(updateTime); + } + } + } + for (ShelfMaterialDetail d : details) { + if (isShelfDetailChildRow(d) && d.getParentUniqueId() != null + && !parentUniqueIdsInOrder.contains(d.getParentUniqueId())) { + d.setShelvesStatus(resolveShelfDetailStatusOnOrderComplete(d.getAlreadyShelvesQuantity(), d.getQuantity())); + d.setUpdateBy(loginUser.getUserid()); + d.setUpdateByName(userRealName); + d.setUpdateTime(updateTime); + } + } + flushShelfMaterialDetailShelvesStatusColumnsOnly(details, loginUser, userRealName); + } + + /** + * 只落库上架状态及审计字段。{@link ShelfMaterialDetail} 查询结果中 total_* 与 alreadyShelves* 曾映射同一列, + * 使用 {@code updateBatchById} 会生成重复的 SET 列名(达梦报错)。此处按主键逐条 {@code update(wrapper)} 仅更新必要列。 + */ + private void flushShelfMaterialDetailShelvesStatusColumnsOnly(List details, LoginUser loginUser, + String userRealName) { + if (CollectionUtils.isEmpty(details) || loginUser == null) { + return; + } + for (ShelfMaterialDetail d : details) { + if (d == null || d.getMaterialDetailId() == null) { + continue; + } + shelfMaterialDetailService.update(null, new LambdaUpdateWrapper() + .set(ShelfMaterialDetail::getShelvesStatus, d.getShelvesStatus()) + .set(ShelfMaterialDetail::getUpdateBy, loginUser.getUserid()) + .set(ShelfMaterialDetail::getUpdateByName, userRealName) + .set(ShelfMaterialDetail::getUpdateTime, d.getUpdateTime() != null ? d.getUpdateTime() : new Date()) + .eq(ShelfMaterialDetail::getMaterialDetailId, d.getMaterialDetailId())); + } + } + + private static boolean isShelfDetailChildRow(ShelfMaterialDetail d) { + return d.getLevel() != null && d.getLevel() == 2 + && d.getParentUniqueId() != null && d.getParentUniqueId() != 0L; + } + + private static BigDecimal nzShelf(BigDecimal v) { + return v != null ? v : BigDecimal.ZERO; + } + + /** 进行中:1 待上架 / 2 上架中 / 3 已上满 / 5 超上 */ + private static Integer deriveShelfDetailStatusWhileOrderOpen(BigDecimal alreadyShelvesQuantity, BigDecimal quantity) { + BigDecimal already = alreadyShelvesQuantity != null ? alreadyShelvesQuantity : BigDecimal.ZERO; + BigDecimal plan = quantity != null ? quantity : BigDecimal.ZERO; + if (already.compareTo(BigDecimal.ZERO) == 0) { + return 1; + } + int cmp = already.compareTo(plan); + if (cmp < 0) { + return 2; + } + if (cmp > 0) { + return 5; + } + return 3; + } + + /** 完成整单:3 已上满 / 4 少上 / 5 超上 */ + private static Integer resolveShelfDetailStatusOnOrderComplete(BigDecimal alreadyShelvesQuantity, BigDecimal quantity) { + BigDecimal already = alreadyShelvesQuantity != null ? alreadyShelvesQuantity : BigDecimal.ZERO; + BigDecimal plan = quantity != null ? quantity : BigDecimal.ZERO; + int cmp = already.compareTo(plan); + if (cmp > 0) { + return 5; + } + if (cmp < 0) { + return 4; + } + return 3; + } + + private static String resolveUserRealNameForShelf(LoginUser loginUser) { + if (loginUser == null) { + return ""; + } + if (loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null + && !loginUser.getUserPo().getUserName().trim().isEmpty()) { + return loginUser.getUserPo().getUserName(); + } + if (loginUser.getRealname() != null && !loginUser.getRealname().trim().isEmpty()) { + return loginUser.getRealname(); + } + return loginUser.getUsername() != null ? loginUser.getUsername() : ""; + } + private synchronized void stockIn(List shelfMaterialDetailDOList, StockShelfOrderDO stockReceiptOrderDO) { for (ShelfMaterialDetail shelfMaterialDetail : shelfMaterialDetailDOList) { Long materialBaseInfoId = shelfMaterialDetail.getMaterialBaseInfoId(); diff --git a/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockReceiptOrder/StockReceiptOrderAppApi.java b/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockReceiptOrder/StockReceiptOrderAppApi.java index c754e9311..40e3234fd 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockReceiptOrder/StockReceiptOrderAppApi.java +++ b/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockReceiptOrder/StockReceiptOrderAppApi.java @@ -6,6 +6,7 @@ import com.mhd.common.core.web.domain.AjaxResult; import com.mhd.common.core.web.page.TableDataInfoApi; import com.mhd.common.security.utils.SecurityUtils; import com.mhd.wms.application.service.stockReceiptOrder.StockReceiptOrderApplicationService; +import com.mhd.wms.domain.receiptMaterialDetail.repository.po.ReceiptMaterialDetailPO; import com.mhd.wms.domain.stockReceiptOrder.repository.po.StockReceiptOrderPO; import com.mhd.wms.domain.stockReceiptOrder.repository.todo.StockReceiptOrderDO; import com.mhd.wms.interfaces.assember.stockReceiptOrder.StockReceiptOrderAssembler; @@ -62,6 +63,20 @@ public class StockReceiptOrderAppApi extends BaseController { return AjaxResult.success(stockReceiptOrderApplicationService.getInfoByApp(receiptOrderId, receiptStatus)); } + /** + * PDA:查询某计划行(卡片)下所有 level=2 分批收货子行(含首笔仅落主行时的主行快照),附带序列号。 + */ + @ApiOperation("按父计划行查询分批收货子行(level=2)") + @GetMapping("/listLevel2ChildrenByParent") + public AjaxResult listLevel2ChildrenByParent( + @ApiParam(value = "收货单ID", required = true) @RequestParam Long receiptOrderId, + @ApiParam(value = "收货明细 material_detail_id(计划行或子行均可,会解析到计划父行)", required = true) + @RequestParam Long materialDetailId) { + List list = stockReceiptOrderApplicationService.listLevel2ChildrenByParentForApp( + receiptOrderId, materialDetailId); + return AjaxResult.success(list); + } + /** * 收货 */ diff --git a/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockShelfOrder/StockShelfOrderAppApi.java b/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockShelfOrder/StockShelfOrderAppApi.java index 37e94a3a9..50bd9dda0 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockShelfOrder/StockShelfOrderAppApi.java +++ b/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApp/stockShelfOrder/StockShelfOrderAppApi.java @@ -5,11 +5,13 @@ import com.mhd.common.core.web.domain.AjaxResult; import com.mhd.common.core.web.page.TableDataInfoApi; import com.mhd.common.security.utils.SecurityUtils; import com.mhd.wms.application.service.stockShelfOrder.StockShelfOrderApplicationService; +import com.mhd.wms.domain.shelfMaterialDetail.repository.po.ShelfMaterialDetailPO; import com.mhd.wms.domain.stockShelfOrder.repository.po.StockShelfOrderPO; import com.mhd.wms.domain.stockShelfOrder.repository.todo.StockShelfOrderDO; import com.mhd.wms.interfaces.assember.stockShelfOrder.StockShelfOrderAssembler; import com.mhd.wms.interfaces.dto.stockShelfOrder.StockShelfOrderDTO; import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -47,7 +49,7 @@ public class StockShelfOrderAppApi extends BaseController { } /** - * 获取上架单详细信息(PDA:主项无子项时添加 parent_copy,供详情页从 children 读取数据) + * 获取上架单详细信息(PDA:与收货 getInfoByApp 一致保留父子结构;子行计划数量在服务端清空,见 StockShelfOrderApplicationService#getInfo) */ @ApiOperation("获取上架单") @GetMapping(value = "/getInfo") @@ -58,6 +60,19 @@ public class StockShelfOrderAppApi extends BaseController { return AjaxResult.success(stockShelfOrderApplicationService.getInfo(shelfOrderId, true, shelfStatus)); } + /** + * PDA:查询某计划行(卡片)下所有 level=2 分批上架子行(含首笔仅落主行时的主行快照),附带序列号。 + */ + @ApiOperation("按父计划行查询分批上架子行(level=2)") + @GetMapping("/listLevel2ChildrenByParent") + public AjaxResult listLevel2ChildrenByParent( + @ApiParam(value = "上架单ID", required = true) @RequestParam Long shelfOrderId, + @ApiParam(value = "上架明细 material_detail_id(计划行或子行均可,会解析到计划父行)", required = true) + @RequestParam Long materialDetailId) { + List list = stockShelfOrderApplicationService.listLevel2ChildrenByParentForApp( + shelfOrderId, materialDetailId); + return AjaxResult.success(list); + } @ApiOperation("上架") @PostMapping("/upShelf")