diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index 21eefe7..b1e79eb 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -91,6 +91,9 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getContractCategory, contractManage.getContractCategory()) + .eq(ContractManage::getPartyA, contractManage.getPartyA()) + .eq(ContractManage::getPartyB, contractManage.getPartyB()) + .ne(Func.isNotEmpty(contractManage.getId()), ContractManage::getId, contractManage.getId())); + if (count > 0) { + throw new ServiceException("相同合同类别、甲方和乙方的合同已存在,不能重复提交"); + } + } + private void validateTemporary(ContractManage contractManage) { TransportBusinessSupport.validateRequired(contractManage.getSignType(), "请选择签约类型"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 4eb319f..fa3efc5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -485,7 +485,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl selectLoadingManagePage(IPage page, LoadingManageVO loadingManage) { IPage entityPage = page(page, buildQuery(loadingManage)); @@ -268,7 +274,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl associatedWaybillIds = waybillIds(loadingManage.getWaybillIdsJson()); syncAssociatedWaybills(loadingManage, STATUS_COMPLETED, new ArrayList<>()); + if (result) { + receivablePayableDetailService.generateForCompletedWaybills(associatedWaybillIds); + } return result; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index 6564d2d..4cd99aa 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -1095,8 +1095,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl row.setQuantityUnit(detail.getQuantityUnit())); + rows.forEach(row -> { + if (Func.isEmpty(row.getQuantityUnit())) { + row.setQuantityUnit(detail.getQuantityUnit()); + } + }); } ReceivablePayableFeeDetailVO result = buildFeeDetail(rows); LinkedHashSet feeItemNames = new LinkedHashSet<>(contractFeeItemNames(detail.getContractId())); @@ -463,29 +468,22 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(waybillIds)) return; for (Long waybillId : waybillIds) { Waybill waybill = waybillService.getById(waybillId); - if (waybill == null || Func.isEmpty(waybill.getContractId())) continue; - ContractManage contract = contractManageService.getById(waybill.getContractId()); - if (contract == null || !isSystemGeneration(contract)) continue; + if (waybill == null) continue; + ContractManage receivableContract = Func.isEmpty(waybill.getContractId()) + ? null : contractManageService.getById(waybill.getContractId()); try { - String planId = matchedPlanId(waybill, contract); - if (Func.isEmpty(planId)) continue; - // 自动生成只按合同计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 - List matchedFees = calculatedFees(waybill, contract, planId, true); - if (matchedFees.isEmpty()) continue; - BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); - for (String settlementType : List.of("payable", "receivable")) { - if (existsByWaybill(waybill.getId(), settlementType)) continue; - ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); - save(detail); - for (ReceivablePayableCargoFee fee : matchedFees) { - ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); - copy.setId(null); - copy.setDetailId(detail.getId()); - cargoFeeMapper.insert(copy); + if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory()) + && isSystemGeneration(receivableContract)) { + generateCompletedWaybillDetail(waybill, receivableContract, "receivable"); + } + if (Func.isNotEmpty(waybill.getCarrierName())) { + ContractManage payableContract = findCarrierContract(waybill); + if (payableContract != null && isSystemGeneration(payableContract)) { + generateCompletedWaybillDetail(waybill, payableContract, "payable"); } } } catch (Exception exception) { - log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, contractId:{}, failureReason:{}", + log.error("自动生成运单费用明细失败,waybillId:{}, waybillNo:{}, receivableContractId:{}, failureReason:{}", waybill.getId(), waybill.getWaybillNo(), waybill.getContractId(), exception.getMessage(), exception); if (exception instanceof RuntimeException runtimeException) { throw runtimeException; @@ -495,6 +493,46 @@ public class ReceivablePayableDetailServiceImpl } } + private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType) { + if (existsByWaybill(waybill.getId(), settlementType)) return; + String planId = matchedPlanId(waybill, contract); + if (Func.isEmpty(planId)) return; + // 自动生成只按合同默认计费方案计算,matchOnly=true 禁止回退读取运单自身其他费用。 + List matchedFees = calculatedFees(waybill, contract, planId, true); + if (matchedFees.isEmpty()) return; + BigDecimal contractUnitPrice = resolveContractUnitPrice(matchedFees); + ReceivablePayableDetail detail = buildDetail(waybill, contract, settlementType, matchedFees, contractUnitPrice); + save(detail); + for (ReceivablePayableCargoFee fee : matchedFees) { + ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); + copy.setId(null); + copy.setDetailId(detail.getId()); + cargoFeeMapper.insert(copy); + } + } + + private ContractManage findCarrierContract(Waybill waybill) { + if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) + || Func.isEmpty(waybill.getCarrierName())) { + return null; + } + // 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。 + ContractManage customerContract = contractManageService.getById(waybill.getContractId()); + if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) { + return null; + } + return contractManageService.getOne(Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, waybill.getProjectId()) + .eq(ContractManage::getPartyA, customerContract.getPartyA()) + .eq(ContractManage::getPartyB, waybill.getCarrierName()) + .eq(ContractManage::getContractCategory, "承运商合同") + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) + .or().ne(ContractManage::getContractStage, "terminated")) + .orderByDesc(ContractManage::getCreateTime), false); + } + @Override @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @@ -549,8 +587,8 @@ public class ReceivablePayableDetailServiceImpl private String matchedPlanId(Waybill waybill, ContractManage contract) { List> plans = parseList(contract.getBillingPlanJson()); if (plans.isEmpty()) return null; - Map plan = plans.stream().filter(item -> Boolean.TRUE.equals(item.get("defaultPlan"))).findFirst() - .orElse(plans.get(plans.size() - 1)); + Map plan = plans.stream().filter(this::isDefaultPlan).findFirst().orElse(null); + if (plan == null) return null; if (!(plan.get("rules") instanceof List rules)) return null; boolean matched = rules.stream().anyMatch(value -> value instanceof Map raw && matchesRule(raw, waybill)); if (!matched) return null; @@ -559,10 +597,21 @@ public class ReceivablePayableDetailServiceImpl private boolean matchesRule(Map raw, Waybill waybill) { Object conditionValue = raw.get("matchCondition"); - if (!(conditionValue instanceof Map condition)) return true; + if (!(conditionValue instanceof Map condition) || !hasConfiguredMatchCondition(condition)) return true; return matchesLocation(condition, "origin", waybill.getDepartureAddressId(), waybill.getDepartureName(), waybill.getDepartureAddress()) && matchesLocation(condition, "destination", waybill.getArrivalAddressId(), waybill.getArrivalName(), waybill.getArrivalAddress()) - && matchesCondition(condition.get("cargoType"), waybill.getCargoType()); + && matchesCondition(condition.get("transportMode"), waybill.getTransportType()) + && matchesCargoType(condition, waybill); + } + + private boolean hasConfiguredMatchCondition(Map condition) { + return !isBlank(condition.get("origin")) + || !isBlank(condition.get("originCode")) + || !isBlank(condition.get("destination")) + || !isBlank(condition.get("destinationCode")) + || !isBlank(condition.get("transportMode")) + || !isBlank(condition.get("cargoType")) + || !isBlank(condition.get("cargoTypeCode")); } private boolean matchesLocation(Map condition, String location, Long addressId, String addressName, @@ -574,10 +623,19 @@ public class ReceivablePayableDetailServiceImpl if (!isBlank(expectedCode) && !isBlank(actualCode)) { return matchesCondition(expectedCode, actualCode); } + if (!isBlank(expectedCode) && isBlank(expectedName)) return false; return matchesCondition(expectedName, addressName) || matchesCondition(expectedName, detailAddress); } + private boolean matchesCargoType(Map condition, Waybill waybill) { + Object expectedName = condition.get("cargoType"); + Object expectedCode = condition.get("cargoTypeCode"); + if (!isBlank(expectedName)) return matchesCondition(expectedName, waybill.getCargoType()); + if (!isBlank(expectedCode)) return matchesCondition(expectedCode, waybill.getCargoType()); + return true; + } + private String resolveRegionCode(Long addressId) { if (addressId == null) return ""; try { @@ -590,6 +648,9 @@ public class ReceivablePayableDetailServiceImpl } private boolean isBlank(Object value) { + if (value instanceof Collection collection) { + return collection.isEmpty() || collection.stream().allMatch(this::isBlank); + } return value == null || String.valueOf(value).trim().isEmpty(); } @@ -641,11 +702,30 @@ public class ReceivablePayableDetailServiceImpl } private LambdaQueryWrapper buildWaybillQuery(ReceivablePayableGenerateRequest request) { + String targetSettlementType = settlementType(request.getSettlementType()); + ContractManage contract = contractManageService.getById(request.getContractId()); + validateContractSettlementType(contract, targetSettlementType); LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(Waybill::getIsDeleted, 0) - .eq(Waybill::getContractId, request.getContractId()) .eq(Waybill::getBusinessStatus, "completed"); - String targetSettlementType = settlementType(request.getSettlementType()); + if ("receivable".equals(targetSettlementType)) { + wrapper.eq(Waybill::getContractId, contract.getId()); + } else { + List customerContractIds = contractManageService.list(Wrappers.lambdaQuery() + .select(ContractManage::getId) + .eq(ContractManage::getIsDeleted, 0) + .eq(ContractManage::getProjectId, contract.getProjectId()) + .eq(ContractManage::getContractCategory, "客户合同") + .eq(ContractManage::getPartyA, contract.getPartyA())) + .stream().map(ContractManage::getId).toList(); + if (customerContractIds.isEmpty()) { + wrapper.eq(Waybill::getId, -1L); + } else { + wrapper.in(Waybill::getContractId, customerContractIds) + .eq(Waybill::getProjectId, contract.getProjectId()) + .eq(Waybill::getCarrierName, contract.getPartyB()); + } + } wrapper.notInSql(Waybill::getId, "select waybill_id from blade_receivable_payable_detail" + " where is_deleted = 0 and waybill_id is not null and settlement_type = '" @@ -687,9 +767,9 @@ public class ReceivablePayableDetailServiceImpl detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId()); detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); - detail.setCustomerName("payable".equals(settlementType) - ? (contract == null ? waybill.getCarrierName() : contract.getPartyA()) - : (contract == null ? waybill.getCustomerName() : contract.getPartyB())); + detail.setCustomerName(contract == null + ? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName()) + : ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA())); detail.setContractId(contract == null ? waybill.getContractId() : contract.getId()); detail.setContractNo(contract == null ? null : contract.getContractNo()); detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName()); @@ -710,8 +790,7 @@ public class ReceivablePayableDetailServiceImpl detail.setOtherFeeAmount(other); detail.setTotalAmount(total); detail.setSettlementStatus("pending"); - detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, - (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); return detail; } @@ -854,14 +933,20 @@ public class ReceivablePayableDetailServiceImpl if (matchOnly && !matchesRule(raw, waybill)) continue; BigDecimal amount = calculateRule(rule, waybill); if (amount == null) continue; + String feeItem = stringValue(rule, "feeItem", "费用"); + Map feeGoods = summarizeFeeGoods(rule, waybill); ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); - fee.setCargoName(stringValue(rule, "feeItem", "费用")); fee.setCargoType(waybill.getCargoType()); + fee.setCargoName(feeGoods.getOrDefault("cargoName", waybill.getCargoName())); + fee.setCargoType(feeGoods.getOrDefault("cargoType", waybill.getCargoType())); + fee.setSpecification(feeGoods.getOrDefault("specification", waybill.getSpecification())); + fee.setModel(feeGoods.getOrDefault("model", waybill.getModel())); fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); - fee.setTransportQuantity(measure(rule, waybill)); fee.setQuantityUnit(waybill.getQuantityUnit()); + fee.setTransportQuantity(measure(rule, waybill)); + fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", waybill.getQuantityUnit())); fee.setPriceUnit(stringValue(rule, "billingUnit", waybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); fee.setMileage(normalizeGeneratedMileage(waybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); - fee.setFeeItemsJson(JsonUtil.toJson(Map.of(fee.getCargoName(), amount))); + fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); result.add(fee); } @@ -894,16 +979,22 @@ public class ReceivablePayableDetailServiceImpl private BigDecimal calculateRule(Map rule, Waybill waybill) { String element = stringValue(rule, "billingElement", ""); String type = stringValue(rule, "billingType", ""); BigDecimal base = measure(rule, waybill); BigDecimal unit = decimal(rule.get("unitPrice")); - if ("按重量".equals(element) || "按吨·公里".equals(element)) { - BigDecimal minimum = decimal(rule.get("minimumBillingWeight")); - if (minimum.signum() > 0 && "按重量".equals(element)) base = base.max(minimum); - if (minimum.signum() > 0 && "按吨·公里".equals(element)) base = base.divide(money(waybill.getQuantity()).max(BigDecimal.ONE), 6, RoundingMode.HALF_UP).multiply(minimum).multiply(money(waybill.getMileage())); - } - if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP); List> ranges = ranges(rule); + boolean intervalUnitPrice = "区间单价".equals(type); + boolean intervalFlatPrice = type.contains("区间") && type.contains("一口价"); + if (!intervalUnitPrice && !intervalFlatPrice) base = applyMinimum(base, rule, waybill); + if ("固定一口价".equals(type)) return unit.setScale(2, RoundingMode.HALF_UP); if (ranges.isEmpty() || "固定单价".equals(type)) return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); - if (type.contains("区间") && type.contains("一口价")) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); - if ("区间单价".equals(type)) { BigDecimal rangeBase = base; return range(ranges, rangeBase).map(r -> decimal(r.get("unitPrice")).multiply(rangeBase)).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); } + if (intervalFlatPrice) return range(ranges, base).map(r -> decimal(r.get("unitPrice")).signum() == 0 ? unit : decimal(r.get("unitPrice"))).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); + if (intervalUnitPrice) { + BigDecimal rangeBase = base; + return range(ranges, rangeBase).map(r -> { + BigDecimal minimum = decimal(r.get("minimumBillingWeight")); + if (minimum.signum() <= 0) minimum = decimal(rule.get("minimumBillingWeight")); + BigDecimal effectiveBase = applyMinimum(rangeBase, element, minimum, waybill); + return decimal(r.get("unitPrice")).multiply(effectiveBase); + }).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP); + } if ("阶梯单价".equals(type)) { BigDecimal total = BigDecimal.ZERO, previous = BigDecimal.ZERO; for (Map r : ranges.stream().sorted(Comparator.comparing(x -> decimal(x.get("lowerLimit")))).toList()) { @@ -916,6 +1007,20 @@ public class ReceivablePayableDetailServiceImpl return base.multiply(unit).setScale(2, RoundingMode.HALF_UP); } + private BigDecimal applyMinimum(BigDecimal base, Map rule, Waybill waybill) { + return applyMinimum(base, stringValue(rule, "billingElement", ""), decimal(rule.get("minimumBillingWeight")), waybill); + } + + private BigDecimal applyMinimum(BigDecimal base, String element, BigDecimal minimum, Waybill waybill) { + if (minimum.signum() <= 0) return base; + if ("按重量".equals(element)) return base.max(minimum); + if ("按吨·公里".equals(element)) { + BigDecimal effectiveWeight = money(waybill.getQuantity()).max(minimum); + return effectiveWeight.multiply(money(waybill.getMileage())); + } + return base; + } + private boolean isFreight(ReceivablePayableCargoFee fee) { return fee.getFreightAmount() != null && fee.getFreightAmount().compareTo(BigDecimal.ZERO) > 0; } @@ -926,21 +1031,91 @@ public class ReceivablePayableDetailServiceImpl } private BigDecimal measure(Map rule, Waybill waybill) { - return switch (stringValue(rule, "billingElement", "按重量")) { - case "按体积" -> volume(waybill); + String element = stringValue(rule, "billingElement", "按重量"); + List> goods = feeGoods(rule, waybill); + return switch (element) { + case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add); case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE; case "按里程" -> money(waybill.getMileage()); - case "按吨·公里" -> money(waybill.getQuantity()).multiply(money(waybill.getMileage())); - case "按数量" -> money(waybill.getQuantity()); - default -> money(waybill.getQuantity()); + case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage())); + case "按数量" -> goodsQuantity(goods); + default -> goodsQuantity(goods); }; } + private List> feeGoods(Map rule, Waybill waybill) { + List> goods = parseList(waybill.getGoodsJson()); + if (goods.isEmpty()) return List.of(); + String element = stringValue(rule, "billingElement", "按重量"); + if ("按体积".equals(element)) { + return goods.stream().filter(this::isVolumeGoods).toList(); + } + if ("按重量".equals(element) || "按吨·公里".equals(element)) { + List> weighted = goods.stream().filter(item -> !isVolumeGoods(item)).toList(); + return weighted.isEmpty() ? goods : weighted; + } + return goods; + } + + private Map summarizeFeeGoods(Map rule, Waybill waybill) { + List> goods = feeGoods(rule, waybill); + if (goods.isEmpty()) return Map.of(); + Map result = new LinkedHashMap<>(); + putCommonGoodsValue(result, "cargoName", goods); + putCommonGoodsValue(result, "cargoType", goods); + putCommonGoodsValue(result, "specification", goods); + putCommonGoodsValue(result, "model", goods); + putCommonGoodsValue(result, "quantityUnit", goods); + return result; + } + + private void putCommonGoodsValue(Map result, String key, List> goods) { + List values = goods.stream().map(item -> stringValue(item, key, "")).filter(Func::isNotEmpty).distinct().toList(); + if (values.size() == 1) result.put(key, values.get(0)); + else if (!values.isEmpty()) result.put(key, String.join(",", values)); + } + + private BigDecimal goodsQuantity(List> goods) { + return goods.stream().map(item -> decimal(item.get("quantity"))).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal goodsVolume(Map goods) { + BigDecimal value = decimal(goods.get("volume")); + if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); + if (value.signum() == 0 && isVolumeGoods(goods)) value = decimal(goods.get("quantity")); + return value; + } + + private boolean isVolumeGoods(Map goods) { + if (decimal(goods.get("volume")).signum() > 0 || decimal(goods.get("cargoVolume")).signum() > 0) return true; + return isVolumeUnit(stringValue(goods, "quantityUnit", "")); + } + private BigDecimal volume(Waybill waybill) { + BigDecimal total = BigDecimal.ZERO; + for (Map goods : parseList(waybill.getGoodsJson())) { + BigDecimal value = decimal(goods.get("volume")); + if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); + if (value.signum() == 0 && isVolumeUnit(stringValue(goods, "quantityUnit"))) { + value = decimal(goods.get("quantity")); + } + total = total.add(value); + } + if (total.signum() > 0) return total; Map goods = parseMap(waybill.getGoodsJson()); BigDecimal value = decimal(goods.get("volume")); if (value.signum() == 0) value = decimal(goods.get("cargoVolume")); - return value; + if (value.signum() == 0 && isVolumeUnit(stringValue(goods, "quantityUnit"))) { + value = decimal(goods.get("quantity")); + } + if (value.signum() > 0) return value; + return isVolumeUnit(waybill.getQuantityUnit()) ? money(waybill.getQuantity()) : BigDecimal.ZERO; + } + + private boolean isVolumeUnit(String unit) { + String normalized = String.valueOf(unit == null ? "" : unit).trim().toLowerCase(); + return normalized.equals("方") || normalized.contains("立方") + || normalized.equals("m3") || normalized.equals("m³") || normalized.equals("m^3"); } private List> ranges(Map rule) { @@ -1062,6 +1237,13 @@ public class ReceivablePayableDetailServiceImpl updateById(detail); } + private String aggregateFeeItemsJson(List fees) { + Map feeItems = new LinkedHashMap<>(); + fees.forEach(fee -> parseMap(fee.getFeeItemsJson()).forEach((name, value) -> + feeItems.merge(name, decimal(value), BigDecimal::add))); + return JsonUtil.toJson(feeItems); + } + private void rebuildDetailFee(ReceivablePayableDetail detail, String billingPlanId) { if (SOURCE_MASTER_ORDER.equals(detail.getSourceType())) { rebuildMasterOrderDetailFee(detail, billingPlanId); @@ -1081,7 +1263,7 @@ public class ReceivablePayableDetailServiceImpl detail.setOtherFeeAmount(total.subtract(freight)); detail.setTotalAmount(total); detail.setUnitPrice(resolveContractUnitPrice(fees)); - detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); updateById(detail); } @@ -1112,8 +1294,7 @@ public class ReceivablePayableDetailServiceImpl detail.setOtherFeeAmount(total.subtract(freight)); detail.setTotalAmount(total); detail.setUnitPrice(resolveContractUnitPrice(fees)); - detail.setFeeItemsJson(JsonUtil.toJson(fees.stream().collect(HashMap::new, - (map, fee) -> map.put(fee.getCargoName(), fee.getAfterAmount()), HashMap::putAll))); + detail.setFeeItemsJson(aggregateFeeItemsJson(fees)); updateById(detail); } @@ -1198,12 +1379,24 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(request.getBillingPlanId())) { throw new ServiceException("请选择计费方案"); } - settlementType(request.getSettlementType()); + String targetSettlementType = settlementType(request.getSettlementType()); + validateContractSettlementType(contractManageService.getById(request.getContractId()), targetSettlementType); if (requireWaybill && Func.isEmpty(request.getWaybillIds())) { throw new ServiceException("请选择需要生成费用的运单"); } } + private void validateContractSettlementType(ContractManage contract, String settlementType) { + if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) { + throw new ServiceException("合同不存在或已删除"); + } + String expectedCategory = "payable".equals(settlementType) ? "承运商合同" : "客户合同"; + if (!expectedCategory.equals(contract.getContractCategory())) { + throw new ServiceException(("payable".equals(settlementType) ? "应付" : "应收") + + "费用只能使用" + expectedCategory); + } + } + private Map waybillMap(Waybill waybill) { Map map = new LinkedHashMap<>(); map.put("id", waybill.getId());