From 762d6e4768873fe1b8035aebab8af22701fde8c2 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Fri, 21 Aug 2026 12:07:21 +0800 Subject: [PATCH] fix bug --- .../ReceivablePayableAdjustFeeRequest.java | 3 + .../entity/ReceivablePayableCargoFee.java | 3 + .../ReceivablePayableDetailController.java | 12 +- .../IReceivablePayableDetailService.java | 4 + .../ReceivablePayableDetailServiceImpl.java | 342 ++++++++++++++++-- ...ade_receivable_payable_detail_20260812.sql | 1 + 6 files changed, 324 insertions(+), 41 deletions(-) diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java index e34efbc..25fa656 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ReceivablePayableAdjustFeeRequest.java @@ -70,5 +70,8 @@ public class ReceivablePayableAdjustFeeRequest implements Serializable { @Schema(description = "备注") private String remark; + + @Schema(description = "变更原因") + private String changeReason; } } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java index 151fd87..9c626d6 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceivablePayableCargoFee.java @@ -105,4 +105,7 @@ public class ReceivablePayableCargoFee extends TenantEntity { @Schema(description = "备注") private String remark; + @Schema(description = "变更原因") + private String changeReason; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java index f72c499..9790e3e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceivablePayableDetailController.java @@ -36,10 +36,12 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; +import org.springblade.transport.pojo.dto.ReceivablePayableFeeCalculateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; +import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; import org.springblade.transport.service.IReceivablePayableDetailService; @@ -119,8 +121,16 @@ public class ReceivablePayableDetailController extends BladeController { return R.success("保存成功"); } - @GetMapping("/transfer-candidates") + @PostMapping("/calculate-adjusted-fee") @ApiOperationSupport(order = 7) + @Operation(summary = "调整费用试算") + public R calculateAdjustedFee( + @RequestBody ReceivablePayableFeeCalculateRequest request) { + return R.data(detailService.calculateAdjustedFee(request)); + } + + @GetMapping("/transfer-candidates") + @ApiOperationSupport(order = 8) @Operation(summary = "转结算候选明细") public R>> transferCandidates(Query query, @RequestParam(required = false) String contractName, diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java index 1dc0ab8..0cae341 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java @@ -25,12 +25,14 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; +import org.springblade.transport.pojo.dto.ReceivablePayableFeeCalculateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest; import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest; import org.springblade.transport.pojo.entity.MasterOrder; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.vo.ReceivablePayableChangeRecordVO; +import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO; import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO; import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO; @@ -58,6 +60,8 @@ public interface IReceivablePayableDetailService extends BaseService> transferCandidates(IPage page, String contractName, String batchNo, 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 44dd80f..8a0135d 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 @@ -38,6 +38,7 @@ import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.MasterOrderMapper; import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest; +import org.springblade.transport.pojo.dto.ReceivablePayableFeeCalculateRequest; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest; import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; @@ -74,6 +75,7 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -125,12 +127,18 @@ public class ReceivablePayableDetailServiceImpl @Override public IPage selectPage(IPage page, ReceivablePayableDetailVO query) { - return ReceivablePayableDetailWrapper.build().pageVO(page(page, buildQuery(query))); + IPage result = ReceivablePayableDetailWrapper.build() + .pageVO(page(page, buildQuery(query))); + fillCargoNames(result.getRecords()); + return result; } @Override public List selectList(ReceivablePayableDetailVO query) { - return ReceivablePayableDetailWrapper.build().listVO(list(buildQuery(query))); + List result = ReceivablePayableDetailWrapper.build() + .listVO(list(buildQuery(query))); + fillCargoNames(result); + return result; } @Override @@ -257,6 +265,9 @@ public class ReceivablePayableDetailServiceImpl if (adjusted.getRemark() != null && adjusted.getRemark().length() > 200) { throw new ServiceException("备注不能超过200个字"); } + if (adjusted.getChangeReason() != null && adjusted.getChangeReason().length() > 300) { + throw new ServiceException("变更原因不能超过300个字"); + } if (Boolean.TRUE.equals(adjusted.getManualFee())) { if (Func.isEmpty(adjusted.getFeeItemName())) { throw new ServiceException("请填写手工费用项目"); @@ -290,6 +301,7 @@ public class ReceivablePayableDetailServiceImpl existing.setAdjustAmount(signedAmount.subtract(money(existing.getOriginalAmount()))); existing.setAfterAmount(signedAmount); existing.setRemark(adjusted.getRemark()); + existing.setChangeReason(adjusted.getChangeReason()); if (newManualRow) { cargoFeeMapper.insert(existing); allRows.add(existing); @@ -316,24 +328,40 @@ public class ReceivablePayableDetailServiceImpl feeItems.put(name, money(amount)); }); } - appendChange(changes, "计费数量", existing.getTransportQuantity(), adjusted.getTransportQuantity()); - appendChange(changes, "里程", existing.getMileage(), adjusted.getMileage()); - appendChange(changes, "运输费", existing.getFreightAmount(), adjusted.getFreightAmount()); + BigDecimal transportQuantity = money(adjusted.getTransportQuantity()); + BigDecimal mileage = money(adjusted.getMileage()); + BigDecimal freightAmount = money(adjusted.getFreightAmount()); + Map effectiveFeeItems = feeItems; + boolean ruleInputChanged = money(existing.getTransportQuantity()).compareTo(transportQuantity) != 0 + || money(existing.getMileage()).compareTo(mileage) != 0; + if (ruleInputChanged) { + AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, existing, transportQuantity, + mileage, freightAmount, effectiveFeeItems); + freightAmount = calculation.freightAmount(); + effectiveFeeItems = calculation.feeItems(); + } + appendChange(changes, "计费数量", existing.getTransportQuantity(), transportQuantity); + appendChange(changes, "里程", existing.getMileage(), mileage); + appendChange(changes, "运输费", existing.getFreightAmount(), freightAmount); if (!Objects.equals(existing.getRemark(), adjusted.getRemark())) { changes.add("【备注】从[" + Objects.toString(existing.getRemark(), "") + "]调整为[" + Objects.toString(adjusted.getRemark(), "") + "]"); } + if (!Objects.equals(existing.getChangeReason(), adjusted.getChangeReason())) { + changes.add("【变更原因】从[" + Objects.toString(existing.getChangeReason(), "") + "]调整为[" + + Objects.toString(adjusted.getChangeReason(), "") + "]"); + } Map oldFeeItems = parseMap(existing.getFeeItemsJson()); for (String name : allowedFeeItems) { - appendChange(changes, name, decimal(oldFeeItems.get(name)), money(feeItems.get(name))); + appendChange(changes, name, decimal(oldFeeItems.get(name)), money(effectiveFeeItems.get(name))); } - BigDecimal freightAmount = money(adjusted.getFreightAmount()); - BigDecimal afterAmount = adjustedAfterAmount(freightAmount, feeItems); - existing.setTransportQuantity(money(adjusted.getTransportQuantity())); - existing.setMileage(money(adjusted.getMileage())); + BigDecimal afterAmount = adjustedAfterAmount(freightAmount, effectiveFeeItems); + existing.setTransportQuantity(transportQuantity); + existing.setMileage(mileage); existing.setFreightAmount(freightAmount); - existing.setFeeItemsJson(JsonUtil.toJson(feeItems)); + existing.setFeeItemsJson(JsonUtil.toJson(effectiveFeeItems)); existing.setRemark(adjusted.getRemark()); + existing.setChangeReason(adjusted.getChangeReason()); existing.setAdjustAmount(afterAmount.subtract(money(existing.getOriginalAmount()))); existing.setAfterAmount(afterAmount); cargoFeeMapper.updateById(existing); @@ -347,6 +375,43 @@ public class ReceivablePayableDetailServiceImpl refreshAdjustedDetail(detail, allRows); } + @Override + public ReceivablePayableCargoFeeVO calculateAdjustedFee(ReceivablePayableFeeCalculateRequest request) { + if (request == null || request.getDetailId() == null || request.getFeeId() == null) { + throw new ServiceException("费用调整试算数据不能为空"); + } + ReceivablePayableDetail detail = getExisting(request.getDetailId()); + if (!"pending".equals(detail.getSettlementStatus())) { + throw new ServiceException("仅待结算明细允许调整试算"); + } + ReceivablePayableCargoFee fee = cargoFeeMapper.selectOne( + Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getId, request.getFeeId()) + .eq(ReceivablePayableCargoFee::getDetailId, detail.getId()) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0)); + if (fee == null) { + throw new ServiceException("费用明细不存在"); + } + validateNonNegative(request.getTransportQuantity(), "运输量"); + validateNonNegative(request.getMileage(), "里程"); + AdjustedFeeCalculation calculation = calculateAdjustedFee(detail, fee, + money(request.getTransportQuantity()), money(request.getMileage()), + money(request.getFreightAmount()), normalizeFeeItems(request.getFeeItems())); + ReceivablePayableCargoFeeVO result = Objects.requireNonNull( + BeanUtil.copyProperties(fee, ReceivablePayableCargoFeeVO.class)); + result.setTransportQuantity(request.getTransportQuantity()); + result.setMileage(request.getMileage()); + result.setFreightAmount(calculation.freightAmount()); + result.setFeeItems(new LinkedHashMap<>(calculation.feeItems())); + BigDecimal afterAmount = adjustedAfterAmount(calculation.freightAmount(), calculation.feeItems()); + result.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount()))); + result.setAfterAmount(afterAmount); + result.setOriginalAmountText(formatMoney(fee.getOriginalAmount())); + result.setAdjustAmountText(formatMoney(result.getAdjustAmount())); + result.setAfterAmountText(formatMoney(afterAmount)); + return result; + } + @Override @Transactional(rollbackFor = Exception.class) public void transferSettlement(ReceivablePayableTransferRequest request) { @@ -412,6 +477,7 @@ public class ReceivablePayableDetailServiceImpl .or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")); IPage detailPage = ReceivablePayableDetailWrapper.build() .pageVO(page(new Page<>(page.getCurrent(), page.getSize()), wrapper)); + fillCargoNames(detailPage.getRecords()); Page> result = new Page<>(detailPage.getCurrent(), detailPage.getSize(), detailPage.getTotal()); result.setRecords(detailPage.getRecords().stream().map(this::candidateMap).toList()); return result; @@ -431,11 +497,12 @@ public class ReceivablePayableDetailServiceImpl validateGenerateRequest(request, true); List waybills = waybillService.list(buildWaybillQuery(request)); ContractManage contract = contractManageService.getById(request.getContractId()); - List fees = waybills.stream() - .skip((page.getCurrent() - 1) * page.getSize()).limit(page.getSize()) + List allFees = waybills.stream() .flatMap(waybill -> calculatedFees(waybill, contract, request.getBillingPlanId()).stream()).toList(); + List fees = allFees.stream() + .skip((page.getCurrent() - 1) * page.getSize()).limit(page.getSize()).toList(); ReceivablePayableFeeDetailVO vo = buildFeeDetail(fees); - vo.setTotal((long) waybills.size()); + vo.setTotal((long) allFees.size()); return vo; } @@ -447,6 +514,8 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(waybills)) { throw new ServiceException("没有可生成费用的运单"); } + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); ContractManage contract = contractManageService.getById(request.getContractId()); for (Waybill waybill : waybills) { if (existsByWaybill(waybill.getId(), settlementType(request.getSettlementType()))) { @@ -456,6 +525,7 @@ public class ReceivablePayableDetailServiceImpl save(detail); calculatedFees(waybill, contract, request.getBillingPlanId()).forEach(fee -> { fee.setDetailId(detail.getId()); + fillGeneratedAuditFields(fee, currentUserId, generateTime); cargoFeeMapper.insert(fee); }); } @@ -466,6 +536,8 @@ public class ReceivablePayableDetailServiceImpl rollbackFor = Exception.class) public void generateForCompletedWaybills(List waybillIds) { if (Func.isEmpty(waybillIds)) return; + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); for (Long waybillId : waybillIds) { Waybill waybill = waybillService.getById(waybillId); if (waybill == null) continue; @@ -474,12 +546,12 @@ public class ReceivablePayableDetailServiceImpl try { if (receivableContract != null && "客户合同".equals(receivableContract.getContractCategory()) && isSystemGeneration(receivableContract)) { - generateCompletedWaybillDetail(waybill, receivableContract, "receivable"); + generateCompletedWaybillDetail(waybill, receivableContract, "receivable", currentUserId, generateTime); } if (Func.isNotEmpty(waybill.getCarrierName())) { ContractManage payableContract = findCarrierContract(waybill); if (payableContract != null && isSystemGeneration(payableContract)) { - generateCompletedWaybillDetail(waybill, payableContract, "payable"); + generateCompletedWaybillDetail(waybill, payableContract, "payable", currentUserId, generateTime); } } } catch (Exception exception) { @@ -493,7 +565,8 @@ public class ReceivablePayableDetailServiceImpl } } - private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType) { + private void generateCompletedWaybillDetail(Waybill waybill, ContractManage contract, String settlementType, + Long currentUserId, Date generateTime) { if (existsByWaybill(waybill.getId(), settlementType)) return; String planId = matchedPlanId(waybill, contract); if (Func.isEmpty(planId)) return; @@ -507,6 +580,7 @@ public class ReceivablePayableDetailServiceImpl ReceivablePayableCargoFee copy = BeanUtil.copyProperties(fee, ReceivablePayableCargoFee.class); copy.setId(null); copy.setDetailId(detail.getId()); + fillGeneratedAuditFields(copy, currentUserId, generateTime); cargoFeeMapper.insert(copy); } } @@ -542,6 +616,8 @@ public class ReceivablePayableDetailServiceImpl } ContractManage contract = contractManageService.getById(masterOrder.getContractId()); if (contract == null || !isSystemGeneration(contract)) return; + Long currentUserId = AuthUtil.getUserId(); + Date generateTime = new Date(); try { List masterGoods = masterOrderGoods(masterOrder); if (masterGoods.isEmpty()) return; @@ -564,6 +640,7 @@ public class ReceivablePayableDetailServiceImpl copy.setId(null); copy.setDetailId(detail.getId()); copy.setWaybillId(null); + fillGeneratedAuditFields(copy, currentUserId, generateTime); cargoFeeMapper.insert(copy); } } @@ -728,8 +805,8 @@ public class ReceivablePayableDetailServiceImpl } 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 = '" - + targetSettlementType + "'"); + + " where is_deleted = 0 and waybill_id is not null" + + " and settlement_type in ('receivable', 'payable')"); if (Func.isNotEmpty(request.getBatchNo())) { wrapper.like(Waybill::getBatchNo, request.getBatchNo()); } @@ -923,34 +1000,87 @@ public class ReceivablePayableDetailServiceImpl private List calculatedFees(Waybill waybill, ContractManage contract, String planId, boolean matchOnly) { List> plans = parseList(contract == null ? null : contract.getBillingPlanJson()); Map plan = resolveBillingPlan(plans, planId); - if (plan == null || !(plan.get("rules") instanceof List)) return matchOnly ? List.of() : List.of(buildCargoFee(null, waybill)); + return calculatedFees(waybill, plan, matchOnly); + } + + private List calculatedFees(Waybill waybill, Map plan, + boolean matchOnly) { + if (plan == null || !(plan.get("rules") instanceof List)) { + return matchOnly ? List.of() : buildCargoFees(waybill); + } List result = new ArrayList<>(); int line = 1; for (Object value : (List) plan.get("rules")) { if (!(value instanceof Map raw)) continue; Map rule = new LinkedHashMap<>(); raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); - 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(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(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(feeItem, amount))); - fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", waybill.getRemark())); - result.add(fee); + for (Waybill feeWaybill : feeWaybills(rule, waybill)) { + if (matchOnly && !matchesRule(raw, feeWaybill)) continue; + BigDecimal amount = calculateRule(rule, feeWaybill); + if (amount == null) continue; + String feeItem = stringValue(rule, "feeItem", "费用"); + Map feeGoods = summarizeFeeGoods(rule, feeWaybill); + ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); + fee.setWaybillId(waybill.getId()); fee.setLineNo(String.format("%04d", line++)); + fee.setCargoName(feeGoods.getOrDefault("cargoName", feeWaybill.getCargoName())); + fee.setCargoType(feeGoods.getOrDefault("cargoType", feeWaybill.getCargoType())); + fee.setSpecification(feeGoods.getOrDefault("specification", feeWaybill.getSpecification())); + fee.setModel(feeGoods.getOrDefault("model", feeWaybill.getModel())); + fee.setBillingFactor(stringValue(rule, "billingElement", "")); fee.setBillingType(stringValue(rule, "billingType", "")); + fee.setTransportQuantity(measure(rule, feeWaybill)); + fee.setQuantityUnit(feeGoods.getOrDefault("quantityUnit", feeWaybill.getQuantityUnit())); + fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit())); fee.setUnitPrice(decimal(rule.get("unitPrice"))); + fee.setMileage(normalizeGeneratedMileage(feeWaybill.getMileage())); fee.setFreightAmount(isFreightRule(rule) ? amount : BigDecimal.ZERO); + fee.setFeeItemsJson(JsonUtil.toJson(Map.of(feeItem, amount))); + fee.setOriginalAmount(amount); fee.setAdjustAmount(BigDecimal.ZERO); fee.setAfterAmount(amount); fee.setRemark(stringValue(rule, "remark", feeWaybill.getRemark())); + result.add(fee); + } } - return result.isEmpty() && !matchOnly ? List.of(buildCargoFee(null, waybill)) : result; + return result.isEmpty() && !matchOnly ? buildCargoFees(waybill) : result; + } + + private List feeWaybills(Map rule, Waybill waybill) { + String element = stringValue(rule, "billingElement", "按重量"); + if (!List.of("按重量", "按体积", "按吨·公里", "按数量").contains(element)) { + return List.of(waybill); + } + List goodsWaybills = goodsWaybills(waybill); + return goodsWaybills.isEmpty() ? List.of(waybill) : goodsWaybills; + } + + private List goodsWaybills(Waybill waybill) { + return parseList(waybill.getGoodsJson()).stream().map(goods -> { + Waybill goodsWaybill = Objects.requireNonNull(BeanUtil.copyProperties(waybill, Waybill.class)); + goodsWaybill.setCargoName(stringValue(goods, "cargoName", waybill.getCargoName())); + goodsWaybill.setCargoType(stringValue(goods, "cargoType", waybill.getCargoType())); + goodsWaybill.setSpecification(stringValue(goods, "specification", waybill.getSpecification())); + goodsWaybill.setModel(stringValue(goods, "model", waybill.getModel())); + goodsWaybill.setQuantity(decimal(goods.get("quantity"))); + goodsWaybill.setQuantityUnit(stringValue(goods, "quantityUnit", waybill.getQuantityUnit())); + goodsWaybill.setUnitPrice(goods.get("unitPrice") == null + ? waybill.getUnitPrice() : decimal(goods.get("unitPrice"))); + goodsWaybill.setPriceUnit(stringValue(goods, "priceUnit", waybill.getPriceUnit())); + goodsWaybill.setGoodsJson(JsonUtil.toJson(List.of(goods))); + return goodsWaybill; + }).toList(); + } + + private List buildCargoFees(Waybill waybill) { + List goodsWaybills = goodsWaybills(waybill); + if (goodsWaybills.isEmpty()) return List.of(buildCargoFee(null, waybill)); + List fees = new ArrayList<>(); + for (int index = 0; index < goodsWaybills.size(); index++) { + Waybill goodsWaybill = goodsWaybills.get(index); + if (index > 0) { + goodsWaybill.setFreightJson(null); + goodsWaybill.setOtherFeeTotal(BigDecimal.ZERO); + } + ReceivablePayableCargoFee fee = buildCargoFee(null, goodsWaybill); + fee.setWaybillId(waybill.getId()); + fee.setLineNo(String.format("%04d", index + 1)); + fees.add(fee); + } + return fees; } private Map resolveBillingPlan(List> plans, String planId) { @@ -1198,6 +1328,104 @@ public class ReceivablePayableDetailServiceImpl } } + private Map normalizeFeeItems(Map feeItems) { + Map result = new LinkedHashMap<>(); + if (feeItems == null) return result; + feeItems.forEach((name, amount) -> { + validateNonNegative(amount, name); + result.put(name, money(amount)); + }); + return result; + } + + private AdjustedFeeCalculation calculateAdjustedFee(ReceivablePayableDetail detail, + ReceivablePayableCargoFee fee, BigDecimal transportQuantity, + BigDecimal mileage, BigDecimal freightAmount, + Map feeItems) { + if ("手工调整".equals(fee.getBillingFactor())) { + throw new ServiceException("手工费用不支持按合同计费规则试算"); + } + ContractManage contract = contractManageService.getById(detail.getContractId()); + if (contract == null) { + throw new ServiceException("关联合同不存在"); + } + Waybill adjustedWaybill = adjustedWaybill(detail, fee, transportQuantity, mileage); + List> rules = matchingAdjustedRules(contract, fee, adjustedWaybill); + if (rules.isEmpty()) { + throw new ServiceException("未找到费用明细对应的合同计费规则,请先更新费用"); + } + Map> results = new LinkedHashMap<>(); + for (Map rule : rules) { + BigDecimal amount = calculateRule(rule, adjustedWaybill); + if (amount != null) results.putIfAbsent(money(amount), rule); + } + if (results.size() != 1) { + throw new ServiceException("费用明细对应多个计费结果,无法唯一试算,请先更新费用"); + } + Map.Entry> calculated = results.entrySet().iterator().next(); + BigDecimal amount = calculated.getKey(); + Map rule = calculated.getValue(); + String feeItem = stringValue(rule, "feeItem", "费用"); + Map calculatedFeeItems = new LinkedHashMap<>(feeItems); + calculatedFeeItems.put(feeItem, amount); + BigDecimal calculatedFreight = isFreightRule(rule) ? amount : freightAmount; + return new AdjustedFeeCalculation(calculatedFreight, calculatedFeeItems); + } + + private Waybill adjustedWaybill(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee, + BigDecimal transportQuantity, BigDecimal mileage) { + Waybill source = detail.getWaybillId() == null ? null : waybillService.getById(detail.getWaybillId()); + Waybill waybill = source == null ? new Waybill() + : Objects.requireNonNull(BeanUtil.copyProperties(source, Waybill.class)); + waybill.setQuantity(transportQuantity); + waybill.setMileage(mileage); + waybill.setCargoName(fee.getCargoName()); + waybill.setCargoType(fee.getCargoType()); + waybill.setSpecification(fee.getSpecification()); + waybill.setModel(fee.getModel()); + waybill.setQuantityUnit(fee.getQuantityUnit()); + waybill.setTransportType(detail.getTransportType()); + Map goods = new LinkedHashMap<>(); + goods.put("cargoName", fee.getCargoName()); + goods.put("cargoType", fee.getCargoType()); + goods.put("specification", fee.getSpecification()); + goods.put("model", fee.getModel()); + goods.put("quantity", transportQuantity); + goods.put("quantityUnit", fee.getQuantityUnit()); + if ("按体积".equals(fee.getBillingFactor())) { + goods.put("volume", transportQuantity); + } + waybill.setGoodsJson(JsonUtil.toJson(List.of(goods))); + return waybill; + } + + private List> matchingAdjustedRules(ContractManage contract, + ReceivablePayableCargoFee fee, Waybill waybill) { + Set feeItemNames = parseMap(fee.getFeeItemsJson()).keySet(); + List> candidates = new ArrayList<>(); + for (Map plan : parseList(contract.getBillingPlanJson())) { + if (!(plan.get("rules") instanceof List rules)) continue; + for (Object value : rules) { + if (!(value instanceof Map raw)) continue; + Map rule = new LinkedHashMap<>(); + raw.forEach((key, item) -> rule.put(String.valueOf(key), item)); + if (!Objects.equals(stringValue(rule, "billingElement"), fee.getBillingFactor()) + || !Objects.equals(stringValue(rule, "billingType"), fee.getBillingType()) + || !feeItemNames.contains(stringValue(rule, "feeItem"))) continue; + if (Func.isNotEmpty(fee.getPriceUnit()) + && !Objects.equals(stringValue(rule, "billingUnit"), fee.getPriceUnit())) continue; + candidates.add(rule); + } + } + List> unitPriceMatched = candidates.stream() + .filter(rule -> decimal(rule.get("unitPrice")).compareTo(money(fee.getUnitPrice())) == 0) + .toList(); + if (!unitPriceMatched.isEmpty()) candidates = unitPriceMatched; + List> conditionMatched = candidates.stream() + .filter(rule -> matchesRule(rule, waybill)).toList(); + return conditionMatched.isEmpty() ? candidates : conditionMatched; + } + private BigDecimal adjustedAfterAmount(BigDecimal freightAmount, Map feeItems) { BigDecimal feeItemTotal = feeItems.values().stream().map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); @@ -1208,6 +1436,9 @@ public class ReceivablePayableDetailServiceImpl return name != null && (name.contains("运费") || name.contains("运输费")); } + private record AdjustedFeeCalculation(BigDecimal freightAmount, Map feeItems) { + } + private void appendChange(List changes, String field, BigDecimal before, BigDecimal after) { BigDecimal oldValue = money(before); BigDecimal newValue = money(after); @@ -1331,6 +1562,37 @@ public class ReceivablePayableDetailServiceImpl changeRecordMapper.insert(record); } + private void fillGeneratedAuditFields(ReceivablePayableCargoFee fee, Long currentUserId, + Date generateTime) { + fee.setUpdateUser(currentUserId); + fee.setUpdateTime(generateTime); + } + + private void fillCargoNames(List details) { + if (Func.isEmpty(details)) return; + List detailIds = details.stream().map(ReceivablePayableDetailVO::getId) + .filter(Objects::nonNull).distinct().toList(); + if (detailIds.isEmpty()) return; + List fees = cargoFeeMapper.selectList( + Wrappers.lambdaQuery() + .select(ReceivablePayableCargoFee::getDetailId, ReceivablePayableCargoFee::getCargoName) + .in(ReceivablePayableCargoFee::getDetailId, detailIds) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .orderByAsc(ReceivablePayableCargoFee::getDetailId) + .orderByAsc(ReceivablePayableCargoFee::getLineNo)); + Map> cargoNames = new LinkedHashMap<>(); + for (ReceivablePayableCargoFee fee : fees) { + if (Func.isEmpty(fee.getCargoName())) continue; + cargoNames.computeIfAbsent(fee.getDetailId(), key -> new LinkedHashSet<>()).add(fee.getCargoName()); + } + details.forEach(detail -> { + Set names = cargoNames.get(detail.getId()); + if (Func.isNotEmpty(names)) { + detail.setCargoName(String.join(",", names)); + } + }); + } + private void applyManualAdjustment(ReceivablePayableDetail detail, BigDecimal amount, String feeItem, String reason) { ReceivablePayableCargoFee fee = new ReceivablePayableCargoFee(); fee.setDetailId(detail.getId()); fee.setWaybillId(detail.getWaybillId()); fee.setLineNo("ADJ-" + System.currentTimeMillis()); diff --git a/doc/sql/transport/blade_receivable_payable_detail_20260812.sql b/doc/sql/transport/blade_receivable_payable_detail_20260812.sql index 4e8a6c0..b9d639e 100644 --- a/doc/sql/transport/blade_receivable_payable_detail_20260812.sql +++ b/doc/sql/transport/blade_receivable_payable_detail_20260812.sql @@ -81,6 +81,7 @@ CREATE TABLE IF NOT EXISTS `blade_receivable_payable_cargo_fee` ( `adjust_amount` decimal(18,2) DEFAULT NULL COMMENT '调整金额', `after_amount` decimal(18,2) DEFAULT NULL COMMENT '调整后总金额', `remark` varchar(200) DEFAULT NULL COMMENT '备注', + `change_reason` varchar(300) DEFAULT NULL COMMENT '变更原因', PRIMARY KEY (`id`) USING BTREE, KEY `idx_receivable_payable_cargo_detail` (`detail_id`) USING BTREE, KEY `idx_receivable_payable_cargo_waybill` (`waybill_id`) USING BTREE