diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java index 353febe3..7cc632d4 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java @@ -6,6 +6,8 @@ */ package org.springblade.transport.pojo.entity; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; @@ -41,6 +43,7 @@ public class TransportReconciliationExternal extends TenantEntity { private BigDecimal freightAmount; private String feeItemsJson; private BigDecimal settlementAmount; + @TableField(updateStrategy = FieldStrategy.ALWAYS) private Boolean suspectedDuplicate; private String matchStatus; private Long matchedInternalId; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java index 918749f6..68aabb25 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java @@ -403,10 +403,19 @@ public final class MkProcessBizHandlers { if (entity == null || "returned".equals(entity.getApprovalStatus())) { return entity != null; } - entity.setApprovalStatus("returned"); - entity.setCurrentNode("审核不通过"); - entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); - return service.updateById(entity); + if ("reviewing".equals(entity.getApprovalStatus())) { + PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest(); + request.setId(id); + service.returnBill(request); + } + PaymentApplication updated = service.getById(id); + if (updated == null) { + return false; + } + updated.setApprovalStatus("returned"); + updated.setCurrentNode("审核不通过"); + updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); + return service.updateById(updated); } } 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 c68b021d..aedcfb5c 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 @@ -314,6 +314,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details = detailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo)); fillDetailWaybillAddresses(details); + fillDetailCargoNames(details); vo.setDetails(details); vo.setSummaryFees(listSummaryFees(id)); vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() @@ -1663,6 +1664,117 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl details) { + if (details == null || details.isEmpty()) { + return; + } + List waybillIds = details.stream().map(FormalSettlementDetail::getWaybillId) + .filter(Objects::nonNull).distinct().toList(); + Map waybillMap = waybillIds.isEmpty() ? Map.of() : waybillService.listByIds(waybillIds).stream() + .collect(Collectors.toMap(Waybill::getId, Function.identity(), (left, right) -> left)); + List detailIds = details.stream().map(FormalSettlementDetail::getId).filter(Objects::nonNull).toList(); + Map> feesByDetail = detailIds.isEmpty() ? Map.of() + : detailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds) + .orderByAsc(FormalSettlementDetailFee::getLineNo)).stream() + .collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId, LinkedHashMap::new, Collectors.toList())); + for (FormalSettlementDetail detail : details) { + Waybill waybill = waybillMap.get(detail.getWaybillId()); + LinkedHashSet goodsNames = new LinkedHashSet<>(); + LinkedHashSet goodsTypes = new LinkedHashSet<>(); + if (waybill != null) { + collectGoodsCargo(waybill.getGoodsJson(), goodsNames, goodsTypes); + } + LinkedHashSet feeNames = new LinkedHashSet<>(); + LinkedHashSet feeTypes = new LinkedHashSet<>(); + for (FormalSettlementDetailFee fee : feesByDetail.getOrDefault(detail.getId(), List.of())) { + addCargoToken(feeNames, fee.getCargoName(), true); + addCargoToken(feeTypes, fee.getCargoType(), true); + } + LinkedHashSet names = goodsNames.size() > 1 ? goodsNames : feeNames.size() > 1 ? feeNames : goodsNames; + LinkedHashSet types = goodsTypes.size() > 1 ? goodsTypes : feeTypes.size() > 1 ? feeTypes : goodsTypes; + if (names.isEmpty()) { + addCargoToken(names, detail.getCargoName(), true); + } + if (types.isEmpty()) { + addCargoToken(types, detail.getCargoType(), true); + } + if (names.size() > 1) { + detail.setCargoName(joinCargo(names)); + } + if (types.size() > 1) { + detail.setCargoType(joinCargo(types)); + } + } + } + + private void persistDetailCargoNames(Long settlementId) { + List details = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlementId) + .orderByAsc(FormalSettlementDetail::getLineNo)); + Map cargoNames = new LinkedHashMap<>(); + Map cargoTypes = new LinkedHashMap<>(); + for (FormalSettlementDetail detail : details) { + cargoNames.put(detail.getId(), detail.getCargoName()); + cargoTypes.put(detail.getId(), detail.getCargoType()); + } + fillDetailCargoNames(details); + for (FormalSettlementDetail detail : details) { + if (Objects.equals(cargoNames.get(detail.getId()), detail.getCargoName()) + && Objects.equals(cargoTypes.get(detail.getId()), detail.getCargoType())) { + continue; + } + detailMapper.update(null, Wrappers.lambdaUpdate() + .eq(FormalSettlementDetail::getId, detail.getId()) + .set(FormalSettlementDetail::getCargoName, detail.getCargoName()) + .set(FormalSettlementDetail::getCargoType, detail.getCargoType())); + } + } + + private void collectGoodsCargo(String goodsJson, Set names, Set types) { + if (Func.isEmpty(goodsJson)) { + return; + } + try { + Object parsed = JsonUtil.parse(goodsJson, List.class); + if (!(parsed instanceof List rows)) { + return; + } + for (Object row : rows) { + if (!(row instanceof Map goods)) { + continue; + } + addCargoToken(names, goods.get("cargoName") == null ? null : String.valueOf(goods.get("cargoName")), false); + addCargoToken(types, goods.get("cargoType") == null ? null : String.valueOf(goods.get("cargoType")), false); + } + } catch (RuntimeException ignored) { + // 货物清单异常时保留明细上已有的货物名称。 + } + } + + private void addCargoToken(Set target, String raw, boolean splitJoined) { + if (Func.isEmpty(raw)) { + return; + } + String[] parts = splitJoined ? raw.split("[,,]") : new String[]{raw}; + for (String part : parts) { + String token = part.trim(); + if (token.isEmpty() || "手工调差".equals(token)) { + continue; + } + target.add(token); + } + } + + private String joinCargo(Set values) { + String joined = String.join(",", values); + return joined.length() > 500 ? joined.substring(0, 500) : joined; } private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index a0d5b94c..b3643674 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -383,6 +383,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId())); int lineNo = 1; @@ -419,7 +421,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); settlementRelationMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); removeById(entity); } + public void removeDraft(Long id) { + PaymentApplication entity = editable(id); + if (isBillPayment(entity.getPaymentMethod())) { + releaseBillBalanceIfOccupied(entity); + } + invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); + recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); + settlementRelationMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); + removeById(entity); + } @Override @Transactional(rollbackFor = Exception.class) public void submit(PaymentApplicationStatusRequest request) { @@ -434,13 +445,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl wrapper) { return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream() + .filter(item -> !releasedBillPayment(item)) .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + /** 汇票付款被驳回后不再占用结算单可付款金额,删除草稿由逻辑删除自然释放。 */ + private boolean releasedBillPayment(PaymentApplication payment) { + return payment != null + && isBillPayment(payment.getPaymentMethod()) + && TransportBusiConstant.RETURNED.equals(payment.getApprovalStatus()); + } + private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) { if ("progress_advance".equals(paymentType)) { return cumulativePreSettlementApplied(referenceId, excludeId); @@ -2053,7 +2095,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl !releasedBillPayment(item)) .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); return relationAmount.add(legacyAmount); } @@ -2093,7 +2137,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0) throw new ServiceException("申请付款金额超过汇票可用余额"); + BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId())); + if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额"); if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内"); } private void useBillBalance(PaymentApplication entity) { BillLedger ledger = lockedBill(entity.getBillLedgerId()); - validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true); - BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() - .eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE")); - if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录"); + validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null); + BillLedgerUsage exists = lockedBillUsage(entity.getId()); + if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) { + throw new ServiceException("该付款申请已生成汇票使用记录"); + } BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP); - ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount)); + ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount).setScale(2, RoundingMode.HALF_UP)); + billLedgerMapper.updateById(ledger); + if (exists == null) { + BillLedgerUsage usage = new BillLedgerUsage(); + usage.setBillLedgerId(ledger.getId()); + usage.setPaymentApplicationId(entity.getId()); + usage.setApplicationNo(entity.getPaymentNo()); + usage.setUsedAmount(amount); + usage.setUseDeptId(entity.getDeptId()); + usage.setUseDeptName(entity.getDeptName()); + usage.setUsageStatus(TransportBusiConstant.APPROVED); + usage.setStatus(1); + billLedgerUsageMapper.insert(usage); + return; + } + exists.setBillLedgerId(ledger.getId()); + exists.setApplicationNo(entity.getPaymentNo()); + exists.setUsedAmount(amount); + exists.setUseDeptId(entity.getDeptId()); + exists.setUseDeptName(entity.getDeptName()); + exists.setUsageStatus(TransportBusiConstant.APPROVED); + billLedgerUsageMapper.updateById(exists); + } + + /** + * 汇票付款申请生成后占用汇票可用余额。同一申请重复保存时只调整差额; + * 更换汇票或改为非汇票付款时,先退回原占用。 + */ + private void syncBillOccupation(PaymentApplication entity) { + BillLedgerUsage usage = lockedBillUsage(entity.getId()); + boolean occupy = isBillPayment(entity.getPaymentMethod()) && entity.getBillLedgerId() != null; + if (!occupy) { + if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) { + releaseUsage(usage); + } + return; + } + BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP); + if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus()) + && Objects.equals(usage.getBillLedgerId(), entity.getBillLedgerId())) { + adjustOccupiedAmount(usage, entity, amount); + return; + } + if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) { + releaseUsage(usage); + } + useBillBalance(entity); + } + + private void adjustOccupiedAmount(BillLedgerUsage usage, PaymentApplication entity, BigDecimal amount) { + BigDecimal delta = amount.subtract(money(usage.getUsedAmount())).setScale(2, RoundingMode.HALF_UP); + BillLedger ledger = lockedBill(usage.getBillLedgerId()); + if (delta.compareTo(BigDecimal.ZERO) > 0 && delta.compareTo(money(ledger.getAvailableBalance())) > 0) { + throw new ServiceException("申请付款金额超过汇票可用余额"); + } + BigDecimal available = money(ledger.getAvailableBalance()).subtract(delta).setScale(2, RoundingMode.HALF_UP); + if (available.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("申请付款金额超过汇票可用余额"); + } + BigDecimal faceAmount = money(ledger.getFaceAmount()); + if (available.compareTo(faceAmount) > 0) { + available = faceAmount; + } + ledger.setAvailableBalance(available); billLedgerMapper.updateById(ledger); - BillLedgerUsage usage = new BillLedgerUsage(); - usage.setBillLedgerId(ledger.getId()); - usage.setPaymentApplicationId(entity.getId()); usage.setApplicationNo(entity.getPaymentNo()); usage.setUsedAmount(amount); usage.setUseDeptId(entity.getDeptId()); usage.setUseDeptName(entity.getDeptName()); - usage.setUsageStatus(TransportBusiConstant.APPROVED); - usage.setStatus(1); - billLedgerUsageMapper.insert(usage); + billLedgerUsageMapper.updateById(usage); + } + + private BigDecimal occupiedAmount(Long paymentApplicationId, Long billLedgerId) { + if (paymentApplicationId == null || billLedgerId == null) { + return BigDecimal.ZERO; + } + BillLedgerUsage usage = activeBillUsage(paymentApplicationId); + if (usage == null || !Objects.equals(usage.getBillLedgerId(), billLedgerId)) { + return BigDecimal.ZERO; + } + return money(usage.getUsedAmount()); + } + + private BillLedgerUsage activeBillUsage(Long paymentApplicationId) { + if (paymentApplicationId == null) { + return null; + } + return billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId) + .eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED) + .last("limit 1")); + } + + private BillLedgerUsage lockedBillUsage(Long paymentApplicationId) { + if (paymentApplicationId == null) { + return null; + } + return billLedgerUsageMapper.selectOne(Wrappers.lambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId) + .last("limit 1 FOR UPDATE")); } private void releaseBillBalance(PaymentApplication entity) { @@ -2139,9 +2273,23 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() + .eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()) + .eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE")); + if (usage == null) { + return; + } + releaseUsage(usage); + } + + private void releaseUsage(BillLedgerUsage usage) { BillLedger ledger = lockedBill(usage.getBillLedgerId()); ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount())) - .min(money(ledger.getFaceAmount()))); + .min(money(ledger.getFaceAmount())).setScale(2, RoundingMode.HALF_UP)); billLedgerMapper.updateById(ledger); usage.setUsageStatus("released"); billLedgerUsageMapper.updateById(usage); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java index e5cbabb4..d1172ab4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java @@ -836,6 +836,12 @@ public class ReceivablePayableDetailServiceImpl if (existsByWaybill(waybill.getId(), targetSettlementType)) { continue; } + if ("payable".equals(targetSettlementType) + && trimToNull(waybill.getCarrierName()) != null + && !matchesWaybillCarrier(contract, waybill)) { + throw new ServiceException("运单【" + Func.toStr(waybill.getWaybillNo(), String.valueOf(waybill.getId())) + + "】的承运商不是所选承运商合同的乙方,不能使用项目内其他承运商合同生成应付"); + } ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), targetSettlementType); detail.setSourceType(SOURCE_MANUAL_GENERATION); save(detail); @@ -1064,11 +1070,20 @@ public class ReceivablePayableDetailServiceImpl List payableWaybills = waybills.stream() .filter(waybill -> !isSelfTransport(waybill)).toList(); if (payableWaybills.isEmpty()) return; + List carrierMatchedWaybills = payableWaybills.stream() + .filter(waybill -> matchesWaybillCarrier(carrierContract, waybill)).toList(); + List otherCarrierWaybills = payableWaybills.stream() + .filter(waybill -> !matchesWaybillCarrier(carrierContract, waybill)).toList(); + if (!otherCarrierWaybills.isEmpty()) { + generateAutomaticWaybillDetails(otherCarrierWaybills, false, + this::resolveCarrierContract, currentUserId, generateTime); + } + if (carrierMatchedWaybills.isEmpty()) return; generateAutomaticDetail(carrierContract, "payable", - () -> existsByLoading(loadingNo, "payable"), payableWaybills, + () -> existsByLoading(loadingNo, "payable"), carrierMatchedWaybills, (fees, unitPrice) -> { normalizeWaybillFeeLines(fees); - return buildLoadingDetail(loadingNo, carrierContract, payableWaybills, fees, unitPrice); + return buildLoadingDetail(loadingNo, carrierContract, carrierMatchedWaybills, fees, unitPrice); }, false, currentUserId, generateTime); } @@ -1118,22 +1133,54 @@ public class ReceivablePayableDetailServiceImpl private ContractManage resolveWaybillCarrierContract(Waybill waybill) { if (Func.isNotEmpty(waybill.getLoadingNo())) return null; + return resolveCarrierContract(waybill); + } + + /** + * 应付只认运单选择的承运商。 + * 运单已记录承运商合同时,乙方必须就是该承运商;否则按承运商名称回查同一项目下的承运商合同。 + * 不允许落到项目里其他承运商合同的客商。 + */ + private ContractManage resolveCarrierContract(Waybill waybill) { + if (waybill == null || isSelfTransport(waybill)) { + return null; + } + String carrierName = trimToNull(waybill.getCarrierName()); if (Func.isNotEmpty(waybill.getCarrierContractId())) { - ContractManage contract = contractManageService.getById(waybill.getCarrierContractId()); - if (contract == null || !"承运商合同".equals(contract.getContractCategory())) { + ContractManage recorded = contractManageService.getById(waybill.getCarrierContractId()); + if (recorded == null || !"承运商合同".equals(recorded.getContractCategory())) { throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确"); } - return contract; + if (carrierName == null || matchesWaybillCarrier(recorded, waybill)) { + return recorded; + } } - return findCarrierContract(waybill); + ContractManage matched = findCarrierContract(waybill); + if (matched != null && matchesWaybillCarrier(matched, waybill)) { + return matched; + } + return null; + } + + private boolean matchesWaybillCarrier(ContractManage contract, Waybill waybill) { + if (contract == null || waybill == null || !"承运商合同".equals(contract.getContractCategory())) { + return false; + } + if (waybill.getProjectId() != null && contract.getProjectId() != null + && !Objects.equals(waybill.getProjectId(), contract.getProjectId())) { + return false; + } + String carrierName = trimToNull(waybill.getCarrierName()); + String partyB = trimToNull(contract.getPartyB()); + return carrierName != null && carrierName.equals(partyB); } private ContractManage findCarrierContract(Waybill waybill) { - if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) - || Func.isEmpty(waybill.getCarrierName())) { + String carrierName = trimToNull(waybill.getCarrierName()); + if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) { return null; } - // 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。 + // 承运商合同甲方应与运单绑定的客户合同甲方一致,乙方必须是运单选择的承运商。 ContractManage customerContract = contractManageService.getById(waybill.getContractId()); if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) { return null; @@ -1142,13 +1189,38 @@ public class ReceivablePayableDetailServiceImpl .eq(ContractManage::getIsDeleted, 0) .eq(ContractManage::getProjectId, waybill.getProjectId()) .eq(ContractManage::getPartyA, customerContract.getPartyA()) - .eq(ContractManage::getPartyB, waybill.getCarrierName()) + .eq(ContractManage::getPartyB, carrierName) .eq(ContractManage::getContractCategory, "承运商合同") .and(wrapper -> wrapper.isNull(ContractManage::getContractStage) .or().ne(ContractManage::getContractStage, "terminated")) .orderByDesc(ContractManage::getCreateTime), false); } + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } + + /** + * 应付客商取运单选择的承运商,避免写成项目内其他承运商合同的乙方。 + */ + private String resolveCounterpartyName(Waybill waybill, ContractManage contract, String settlementType) { + if ("payable".equals(settlementType)) { + String carrierName = trimToNull(waybill == null ? null : waybill.getCarrierName()); + if (carrierName != null) { + return carrierName; + } + return contract == null ? null : contract.getPartyB(); + } + if (contract != null && Func.isNotEmpty(contract.getPartyA())) { + return contract.getPartyA(); + } + return waybill == null ? null : waybill.getCustomerName(); + } + @Override @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW, rollbackFor = Exception.class) @@ -1201,12 +1273,24 @@ public class ReceivablePayableDetailServiceImpl .collect(Collectors.toMap(ContractManage::getId, contract -> contract)); Map> carrierWaybills = waybills.stream().collect(Collectors.groupingBy( Waybill::getCarrierContractId, LinkedHashMap::new, Collectors.toList())); + List mismatchedCarrierWaybills = waybills.stream() + .filter(waybill -> !matchesWaybillCarrier(carrierContracts.get(waybill.getCarrierContractId()), waybill)) + .toList(); + if (!mismatchedCarrierWaybills.isEmpty()) { + generateAutomaticWaybillDetails(mismatchedCarrierWaybills, false, + this::resolveCarrierContract, currentUserId, generateTime); + } for (Map.Entry> entry : carrierWaybills.entrySet()) { ContractManage carrierContract = carrierContracts.get(entry.getKey()); if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) { throw new ServiceException("总单【" + masterOrder.getMasterNo() + "】记录的承运商合同不存在或合同类别不正确"); } - List currentCarrierWaybills = entry.getValue(); + List currentCarrierWaybills = entry.getValue().stream() + .filter(waybill -> matchesWaybillCarrier(carrierContract, waybill)) + .toList(); + if (currentCarrierWaybills.isEmpty()) { + continue; + } generateAutomaticDetail(carrierContract, "payable", () -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()), currentCarrierWaybills, @@ -1635,9 +1719,7 @@ 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(contract == null - ? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName()) - : ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA())); + detail.setCustomerName(resolveCounterpartyName(waybill, contract, settlementType)); detail.setContractId(contract == null ? waybill.getContractId() : contract.getId()); detail.setContractNo(contract == null ? null : contract.getContractNo()); detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName()); @@ -1652,8 +1734,8 @@ public class ReceivablePayableDetailServiceImpl detail.setArrivalContact(waybill.getArrivalContact()); detail.setArrivalPhone(waybill.getArrivalPhone()); detail.setTransportType(waybill.getTransportType()); - detail.setCargoName(waybill.getCargoName()); - detail.setCargoType(waybill.getCargoType()); + detail.setCargoName(joinDistinctGoods(waybill, "cargoName", waybill.getCargoName())); + detail.setCargoType(joinDistinctGoods(waybill, "cargoType", waybill.getCargoType())); detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill)); detail.setQuantityUnit(waybill.getQuantityUnit()); detail.setMileage(normalizeGeneratedMileage(waybill.getMileage())); @@ -1668,6 +1750,21 @@ public class ReceivablePayableDetailServiceImpl return detail; } + /** 一单多货时主表只保留首行货物,应付明细改记全部货物名称、货物类型。 */ + private String joinDistinctGoods(Waybill waybill, String field, String fallback) { + LinkedHashSet values = new LinkedHashSet<>(); + for (Map goods : parseList(waybill.getGoodsJson())) { + String value = stringValue(goods, field, ""); + if (Func.isNotEmpty(value)) { + values.add(value.trim()); + } + } + if (values.isEmpty()) { + return fallback; + } + return String.join(",", values); + } + /** * 多货物运单优先按 goodsJson 数量合计作为运输总量,避免主表 quantity 只落了首行。 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java index c22902cc..dff1f39b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java @@ -1233,14 +1233,67 @@ public class TransportReconciliationServiceImpl private List assertAllMatched(TransportReconciliation bill) { List internals = internalRows(bill.getId()); List externals = externalRows(bill.getId()); - if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size() - || internals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchResult())) - || externals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchStatus()) || Boolean.TRUE.equals(item.getSuspectedDuplicate()))) { + // 调整账单后旧的疑似重复标记可能已失效,按当前内容重算,避免已一一匹配仍被拦截 + syncSuspectedDuplicates(externals); + if (!oneToOneMatched(internals, externals)) { throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复"); } return internals; } + private void syncSuspectedDuplicates(List externals) { + Map> groups = externals.stream() + .collect(Collectors.groupingBy(this::duplicateKey)); + Set duplicateIds = new HashSet<>(); + for (List group : groups.values()) { + if (group.size() > 1) { + group.stream().map(TransportReconciliationExternal::getId).forEach(duplicateIds::add); + } + } + for (TransportReconciliationExternal external : externals) { + boolean duplicate = duplicateIds.contains(external.getId()); + String matchStatus = external.getMatchStatus(); + if (!duplicate && DUPLICATE.equals(matchStatus) && external.getMatchedInternalId() != null) { + matchStatus = MATCHED; + } + if (Boolean.TRUE.equals(external.getSuspectedDuplicate()) == duplicate + && Objects.equals(matchStatus, external.getMatchStatus())) { + continue; + } + external.setSuspectedDuplicate(duplicate); + external.setMatchStatus(matchStatus); + externalMapper.update(null, Wrappers.lambdaUpdate() + .eq(TransportReconciliationExternal::getId, external.getId()) + .set(TransportReconciliationExternal::getSuspectedDuplicate, duplicate) + .set(TransportReconciliationExternal::getMatchStatus, matchStatus)); + } + } + + private boolean oneToOneMatched(List internals, + List externals) { + if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size()) { + return false; + } + if (externals.stream().anyMatch(item -> Boolean.TRUE.equals(item.getSuspectedDuplicate()))) { + return false; + } + Map externalMap = externals.stream() + .collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity(), (left, right) -> left)); + Set usedExternalIds = new HashSet<>(); + for (TransportReconciliationInternal internal : internals) { + if (!MATCHED.equals(internal.getMatchResult()) || internal.getMatchedExternalId() == null + || !usedExternalIds.add(internal.getMatchedExternalId())) { + return false; + } + TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId()); + if (external == null || !MATCHED.equals(external.getMatchStatus()) + || !Objects.equals(external.getMatchedInternalId(), internal.getId())) { + return false; + } + } + return usedExternalIds.size() == externals.size(); + } + private void refreshStats(Long id) { TransportReconciliation bill = existing(id); List internals = internalRows(id);