From 8c5704caba65f45b2f6d59e1b8bfbc4d54fbd942 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 24 Aug 2026 00:58:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E9=A2=84=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E3=80=81=E6=AD=A3=E5=BC=8F=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/dto/FormalSettlementSaveRequest.java | 12 + .../transport/pojo/entity/CommonRoute.java | 18 ++ .../entity/FormalSettlementSummaryFee.java | 40 +++ .../transport/pojo/vo/FormalSettlementVO.java | 2 + .../FormalSettlementController.java | 47 ++- .../FormalSettlementSummaryFeeMapper.java | 20 ++ .../service/IFormalSettlementService.java | 1 + .../service/IPreSettlementService.java | 9 +- .../service/impl/CommonRouteServiceImpl.java | 32 ++ .../impl/FormalSettlementServiceImpl.java | 273 +++++++++++++++++- .../impl/PreSettlementServiceImpl.java | 63 +++- ..._common_route_region_id_patch_20260823.sql | 7 + .../blade_formal_settlement_20260818.sql | 16 + ...formal_settlement_summary_fee_20260823.sql | 24 ++ doc/sql/transport/blade_tms_business.sql | 6 + 15 files changed, 530 insertions(+), 40 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java create mode 100644 doc/sql/transport/blade_common_route_region_id_patch_20260823.sql create mode 100644 doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index 5bb3c82..6eb8f18 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -28,4 +28,16 @@ public class FormalSettlementSaveRequest implements Serializable { private BigDecimal exchangeRate; private String attachmentsJson; private String remark; + private List summaryFees; + + @Data + public static class SummaryFee implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String feeType; + private String feeItem; + private BigDecimal adjustAmount; + private String remark; + private Integer manualFlag; + } } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java index 54f4992..4671cb9 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CommonRoute.java @@ -57,6 +57,15 @@ public class CommonRoute extends TenantEntity { @Schema(description = "发货地") private String departureName; + @Schema(description = "发货省ID") + private String departureProvinceId; + + @Schema(description = "发货市ID") + private String departureCityId; + + @Schema(description = "发货区ID") + private String departureDistrictId; + @Schema(description = "发货地址") private String departureAddress; @@ -78,6 +87,15 @@ public class CommonRoute extends TenantEntity { @Schema(description = "收货地") private String arrivalName; + @Schema(description = "收货省ID") + private String arrivalProvinceId; + + @Schema(description = "收货市ID") + private String arrivalCityId; + + @Schema(description = "收货区ID") + private String arrivalDistrictId; + @Schema(description = "收货地址") private String arrivalAddress; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java new file mode 100644 index 0000000..9292f68 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSummaryFee.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算合计费用实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_summary_fee") +@Schema(description = "正式结算合计费用") +public class FormalSettlementSummaryFee extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Integer lineNo; + private String feeType; + private String feeItem; + private BigDecimal originalAmount; + private BigDecimal adjustAmount; + private BigDecimal settlementAmount; + private String remark; + private Integer manualFlag; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index 8dcd0fd..ba68b38 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -15,6 +15,7 @@ import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serial; @@ -39,5 +40,6 @@ public class FormalSettlementVO extends FormalSettlement { @TableField(exist = false) private String preSettlementNo; @TableField(exist = false) private List sources; @TableField(exist = false) private List details; + @TableField(exist = false) private List summaryFees; @TableField(exist = false) private List payments; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java index 85b1a0f..b40f671 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -73,64 +73,79 @@ public class FormalSettlementController extends BladeController { @GetMapping("/contract-options") @ApiOperationSupport(order = 4) @Operation(summary = "可选合同") - public R>> contractOptions(@RequestParam(required = false) String keyword) { - return R.data(preSettlementService.contractOptions(keyword)); + public R>> contractOptions(@RequestParam(required = false) String keyword, + @RequestParam(required = false) Long projectId) { + return R.data(preSettlementService.contractOptions(keyword, projectId)); + } + + @GetMapping("/next-no") + @ApiOperationSupport(order = 5) + @Operation(summary = "获取最新正式结算单号") + public R nextNo() { + return R.data(formalSettlementService.nextNo()); + } + + @GetMapping("/fee-options") + @ApiOperationSupport(order = 6) + @Operation(summary = "可选费用类型及费用项") + public R>> feeOptions() { + return R.data(preSettlementService.feeOptions()); } @GetMapping("/candidate-details") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 7) @Operation(summary = "可选应收应付明细") public R>> candidateDetails(Query query, @RequestParam Long contractId, @RequestParam String settlementType, @RequestParam(required = false) String batchNo, - @RequestParam(required = false) String feeStartDate, @RequestParam(required = false) String feeEndDate) { - return R.data(preSettlementService.candidateDetails(Condition.getPage(query), contractId, - settlementType, batchNo, feeStartDate, feeEndDate)); + @RequestParam(required = false) String createStartDate, @RequestParam(required = false) String createEndDate) { + return R.data(preSettlementService.candidateDetailsByCreateTime(Condition.getPage(query), contractId, + settlementType, batchNo, createStartDate, createEndDate)); } @PostMapping("/save") - @ApiOperationSupport(order = 6) + @ApiOperationSupport(order = 8) @Operation(summary = "保存正式结算草稿") public R save(@RequestBody FormalSettlementSaveRequest request) { return R.data(formalSettlementService.saveDraft(request)); } @PostMapping("/remove") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 9) @Operation(summary = "删除正式结算草稿") public R remove(@RequestParam Long id) { formalSettlementService.removeDraft(id); return R.success("删除成功"); } @PostMapping("/submit") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 10) @Operation(summary = "提交审批") public R submit(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.submit(request); return R.success("提交成功"); } @PostMapping("/approve") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 11) @Operation(summary = "审批通过") public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); } @PostMapping("/return") - @ApiOperationSupport(order = 10) + @ApiOperationSupport(order = 12) @Operation(summary = "审批驳回") public R returnBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.returnBill(request); return R.success("已驳回"); } @PostMapping("/void") - @ApiOperationSupport(order = 11) + @ApiOperationSupport(order = 13) @Operation(summary = "作废正式结算单") public R voidBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.voidBill(request); return R.success("作废成功"); } @PostMapping("/sync-kingdee") - @ApiOperationSupport(order = 12) + @ApiOperationSupport(order = 14) @Operation(summary = "推送金蝶应付单") public R syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); } @GetMapping("/detail-fees") - @ApiOperationSupport(order = 13) + @ApiOperationSupport(order = 15) @Operation(summary = "正式结算货物费用快照") public R> detailFees(@RequestParam Long detailId) { return R.data(formalSettlementService.detailFees(detailId)); } @PostMapping("/adjust-detail") - @ApiOperationSupport(order = 14) + @ApiOperationSupport(order = 16) @Operation(summary = "调整草稿结算明细") public R adjustDetail(@RequestBody PreSettlementDetailAdjustRequest request) { formalSettlementService.adjustDetail(request); @@ -138,7 +153,7 @@ public class FormalSettlementController extends BladeController { } @PostMapping("/apply-payment") - @ApiOperationSupport(order = 15) + @ApiOperationSupport(order = 17) @Operation(summary = "发起尾款付款申请") public R applyPayment(@RequestBody FormalSettlementPaymentRequest request) { return R.data(formalSettlementService.applyPayment(request)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java new file mode 100644 index 0000000..936febe --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSummaryFeeMapper.java @@ -0,0 +1,20 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; + +/** + * 正式结算合计费用 Mapper + * + * @author Chill + */ +public interface FormalSettlementSummaryFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java index 531d798..cc65b20 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -29,6 +29,7 @@ import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; public interface IFormalSettlementService extends BaseService { IPage selectPage(IPage page, FormalSettlementVO query); IPage candidatePreSettlements(IPage page, PreSettlementVO query); + String nextNo(); FormalSettlementVO detail(Long id); Long saveDraft(FormalSettlementSaveRequest request); void removeDraft(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java index 4133a41..28af8ad 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java @@ -33,13 +33,20 @@ public interface IPreSettlementService extends BaseService { PreSettlementVO detail(Long id); - List> contractOptions(String keyword); + default List> contractOptions(String keyword) { + return contractOptions(keyword, null); + } + + List> contractOptions(String keyword, Long projectId); List> feeOptions(); IPage> candidateDetails(IPage page, Long contractId, String settlementType, String batchNo, String feeStartDate, String feeEndDate); + IPage> candidateDetailsByCreateTime(IPage page, Long contractId, + String settlementType, String batchNo, String createStartDate, String createEndDate); + Long saveDraft(PreSettlementSaveRequest request); void removeDraft(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java index b808705..84a04e1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CommonRouteServiceImpl.java @@ -194,10 +194,16 @@ public class CommonRouteServiceImpl extends BaseServiceImpl queryWrapper = Wrappers.lambdaQuery() .eq(CommonRoute::getIsDeleted, 0) 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 fa3efc5..8e49cda 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 @@ -21,10 +21,12 @@ import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; import org.springblade.transport.mapper.PreSettlementDetailMapper; import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; @@ -35,11 +37,13 @@ import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springblade.transport.pojo.entity.FormalSettlementPayment; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.Waybill; @@ -47,6 +51,7 @@ import org.springblade.transport.pojo.vo.FormalSettlementVO; import org.springblade.transport.pojo.vo.PreSettlementVO; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.service.IPreSettlementService; import org.springblade.transport.service.IWaybillService; import org.springblade.transport.wrapper.PreSettlementWrapper; import org.springblade.core.tool.jackson.JsonUtil; @@ -55,12 +60,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -79,15 +89,18 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl numbers = list(Wrappers.lambdaQuery() + .select(FormalSettlement::getFormalSettlementNo) + .likeRight(FormalSettlement::getFormalSettlementNo, prefix)) + .stream().map(FormalSettlement::getFormalSettlementNo).toList(); + int sequence = 1; + if (!numbers.isEmpty()) { + String latest = numbers.stream().max(String::compareTo).orElse(prefix); + try { + sequence = Integer.parseInt(latest.substring(prefix.length())) + 1; + } catch (RuntimeException ignored) { + sequence = Math.toIntExact(count(Wrappers.lambdaQuery() + .likeRight(FormalSettlement::getFormalSettlementNo, prefix))) + 1; + } + } + return prefix + String.format("%04d", sequence); + } + @Override public FormalSettlementVO detail(Long id) { FormalSettlement settlement = existing(id); @@ -138,6 +171,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo))); + vo.setSummaryFees(listSummaryFees(id)); vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime))); return vo; @@ -192,6 +226,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() .in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)); detailMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, id)); + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, id)); removeById(id); } @@ -312,14 +351,232 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() - .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream() - .map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + rebuildSummaryFees(settlement.getId()); + refreshSettlementAmount(settlement); + } + + private void rebuildSummaryFees(Long settlementId) { + List existingRows = listSummaryFees(settlementId); + Map existingGenerated = existingRows.stream() + .filter(row -> !Integer.valueOf(1).equals(row.getManualFlag())) + .collect(Collectors.toMap(this::summaryKey, Function.identity(), (first, second) -> first)); + List existingManualRows = existingRows.stream() + .filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList(); + Map aggregates = new LinkedHashMap<>(); + for (FormalSettlementSource source : sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, settlementId))) { + List sourceSummary = preSummaryFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, source.getPreSettlementId()) + .eq(PreSettlementSummaryFee::getIsDeleted, 0)); + sourceSummary.forEach(row -> { + BigDecimal additionalAmount = Integer.valueOf(1).equals(row.getManualFlag()) + ? money(row.getSettlementAmount()) : money(row.getAdjustAmount()); + aggregates.merge(summaryKey(row.getFeeType(), row.getFeeItem()), additionalAmount, BigDecimal::add); + }); + } + Map feeTypeMap = feeTypeMap(); + for (FormalSettlementDetail detail : detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlementId))) { + List detailFees = detailFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()) + .eq(FormalSettlementDetailFee::getIsDeleted, 0)); + appendFeeAggregates(aggregates, feeTypeMap, detailFees); + } + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, settlementId) + .eq(FormalSettlementSummaryFee::getManualFlag, 0)); + int lineNo = 1; + for (Map.Entry entry : aggregates.entrySet()) { + String[] keyParts = entry.getKey().split("@@@", 2); + FormalSettlementSummaryFee old = existingGenerated.get(entry.getKey()); + FormalSettlementSummaryFee row = new FormalSettlementSummaryFee(); + row.setFormalSettlementId(settlementId); + row.setFeeType(keyParts[0]); + row.setFeeItem(keyParts.length > 1 ? keyParts[1] : ""); + row.setOriginalAmount(money(entry.getValue())); + row.setAdjustAmount(old == null ? BigDecimal.ZERO.setScale(2) : money(old.getAdjustAmount())); + row.setSettlementAmount(row.getOriginalAmount().add(row.getAdjustAmount())); + row.setRemark(old == null ? "" : old.getRemark()); + row.setManualFlag(0); + row.setLineNo(lineNo++); + summaryFeeMapper.insert(row); + } + for (FormalSettlementSummaryFee manualRow : existingManualRows) { + manualRow.setLineNo(lineNo++); + summaryFeeMapper.updateById(manualRow); + } + renumberSummaryFees(settlementId); + } + + private void appendFeeAggregates(Map aggregates, Map feeTypeMap, + List detailFees) { + for (FormalSettlementDetailFee fee : detailFees) { + Map feeItems = parseFeeItems(fee.getFeeItemsJson()); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + BigDecimal knownAmount = feeItems.values().stream().map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + if (!containsFreight) { + BigDecimal freightAmount = money(fee.getFreightAmount()); + aggregates.merge(summaryKey("物流配送", "运输费"), freightAmount, BigDecimal::add); + knownAmount = knownAmount.add(freightAmount); + } + feeItems.forEach((feeItem, amount) -> aggregates.merge( + summaryKey(feeTypeMap.getOrDefault(feeItem, + isFreightFeeItem(feeItem) ? "物流配送" : "其他费用"), feeItem), + money(amount), BigDecimal::add)); + BigDecimal residualAmount = money(fee.getSettlementAmountTax()).subtract(knownAmount); + if (residualAmount.signum() != 0) { + aggregates.merge(summaryKey("其他费用", "其他费用"), residualAmount, BigDecimal::add); + } + } + } + + private void applySummaryRequest(Long settlementId, List requestRows) { + if (requestRows == null) return; + Map> allowedManualFees = new LinkedHashMap<>(); + for (Map option : preSettlementService.feeOptions()) { + Set feeItems = new LinkedHashSet<>(); + if (option.get("feeItems") instanceof List values) { + values.forEach(value -> feeItems.add(String.valueOf(value))); + } + allowedManualFees.put(String.valueOf(option.get("feeType")), feeItems); + } + Map existingMap = listSummaryFees(settlementId).stream() + .collect(Collectors.toMap(FormalSettlementSummaryFee::getId, Function.identity())); + List existingManualRows = existingMap.values().stream() + .filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList(); + Set retainedManualIds = new LinkedHashSet<>(); + int nextLineNo = existingMap.size() + 1; + for (FormalSettlementSaveRequest.SummaryFee requestRow : requestRows) { + if (Integer.valueOf(1).equals(requestRow.getManualFlag())) { + String feeType = requiredText(requestRow.getFeeType(), "费用类型"); + String feeItem = requiredText(requestRow.getFeeItem(), "费用项"); + if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) { + throw new ServiceException("费用类型与费用项不匹配或费用项已停用"); + } + FormalSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId()); + if (row == null && requestRow.getId() == null) row = new FormalSettlementSummaryFee(); + if (row == null || row.getId() != null && !Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的手工费用行"); + } + row.setFormalSettlementId(settlementId); + row.setFeeType(feeType); + row.setFeeItem(feeItem); + row.setOriginalAmount(BigDecimal.ZERO.setScale(2)); + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + row.setSettlementAmount(row.getAdjustAmount()); + row.setRemark(limit(requestRow.getRemark(), 50)); + row.setManualFlag(1); + if (row.getId() == null) { + row.setLineNo(nextLineNo++); + summaryFeeMapper.insert(row); + } else { + summaryFeeMapper.updateById(row); + } + retainedManualIds.add(row.getId()); + continue; + } + FormalSettlementSummaryFee row = existingMap.get(requestRow.getId()); + if (row == null) { + row = existingMap.values().stream() + .filter(item -> !Integer.valueOf(1).equals(item.getManualFlag())) + .filter(item -> Objects.equals(summaryKey(item), + summaryKey(requestRow.getFeeType(), requestRow.getFeeItem()))) + .findFirst().orElse(null); + } + if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的结算合计行"); + } + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + row.setSettlementAmount(money(row.getOriginalAmount()).add(row.getAdjustAmount())); + row.setRemark(limit(requestRow.getRemark(), 50)); + summaryFeeMapper.updateById(row); + } + for (FormalSettlementSummaryFee manualRow : existingManualRows) { + if (!retainedManualIds.contains(manualRow.getId())) summaryFeeMapper.deleteById(manualRow.getId()); + } + renumberSummaryFees(settlementId); + } + + private void refreshSettlementAmount(FormalSettlement settlement) { + BigDecimal amount = listSummaryFees(settlement.getId()).stream() + .map(FormalSettlementSummaryFee::getSettlementAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); settlement.setSettlementAmount(amount); - settlement.setLocalSettlementAmount(amount.multiply(settlement.getExchangeRate() == null ? BigDecimal.ONE : settlement.getExchangeRate())); + settlement.setLocalSettlementAmount(amount.multiply( + settlement.getExchangeRate() == null ? BigDecimal.ONE : settlement.getExchangeRate())); updateById(settlement); } + private List listSummaryFees(Long settlementId) { + return summaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSummaryFee::getFormalSettlementId, settlementId) + .eq(FormalSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(FormalSettlementSummaryFee::getLineNo)); + } + + private void renumberSummaryFees(Long settlementId) { + List rows = listSummaryFees(settlementId); + for (int index = 0; index < rows.size(); index++) { + FormalSettlementSummaryFee row = rows.get(index); + row.setLineNo(index + 1); + summaryFeeMapper.updateById(row); + } + } + + private Map feeTypeMap() { + Map result = new LinkedHashMap<>(); + for (Map option : preSettlementService.feeOptions()) { + if (option.get("feeItems") instanceof List values) { + values.forEach(value -> result.putIfAbsent(String.valueOf(value), + String.valueOf(option.get("feeType")))); + } + } + return result; + } + + private Map parseFeeItems(String json) { + Map result = new LinkedHashMap<>(); + if (Func.isEmpty(json)) return result; + try { + Object parsed = JsonUtil.parse(json, Map.class); + if (parsed instanceof Map map) { + map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value))); + } + } catch (RuntimeException ignored) { + // 兼容历史费用JSON,不影响正式结算保存。 + } + return result; + } + + private BigDecimal decimal(Object value) { + if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO; + try { + return new BigDecimal(String.valueOf(value)); + } catch (NumberFormatException ignored) { + return BigDecimal.ZERO; + } + } + + private boolean isFreightFeeItem(String name) { + return name != null && (name.contains("运费") || name.contains("运输费")); + } + + private String summaryKey(FormalSettlementSummaryFee row) { + return summaryKey(row.getFeeType(), row.getFeeItem()); + } + + private String summaryKey(String feeType, String feeItem) { + return String.valueOf(feeType) + "@@@" + String.valueOf(feeItem); + } + + private String requiredText(String value, String field) { + if (Func.isEmpty(value)) throw new ServiceException(field + "不能为空"); + return value.trim(); + } + private void rebuildSnapshots(FormalSettlement settlement, List sources, List directDetails) { List existingDetailIds = detailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream().map(FormalSettlementDetail::getId).toList(); @@ -490,12 +747,6 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery().likeRight(FormalSettlement::getFormalSettlementNo, prefix)); - return prefix + String.format("%04d", count + 1); - } - private synchronized String nextPaymentNo() { String prefix = "FK" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = paymentMapper.selectCount(Wrappers.lambdaQuery() @@ -503,7 +754,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl> contractOptions(String keyword) { + public List> contractOptions(String keyword, Long projectId) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(ContractManage::getIsDeleted, 0) .in(ContractManage::getApprovalStatus, "approved", "change_approved") .ne(ContractManage::getContractStage, "terminated") + .eq(projectId != null, ContractManage::getProjectId, projectId) .and(Func.isNotEmpty(keyword), query -> query.like(ContractManage::getContractName, keyword) .or().like(ContractManage::getContractNo, keyword)) .orderByDesc(ContractManage::getCreateTime); @@ -201,12 +202,28 @@ public class PreSettlementServiceImpl extends BaseServiceImpl> candidateDetails(IPage page, Long contractId, String settlementType, String batchNo, String feeStartDate, String feeEndDate) { + return candidateDetails(page, contractId, settlementType, batchNo, feeStartDate, feeEndDate, + false, true); + } + + @Override + public IPage> candidateDetailsByCreateTime(IPage page, Long contractId, + String settlementType, String batchNo, String createStartDate, String createEndDate) { + return candidateDetails(page, contractId, settlementType, batchNo, createStartDate, createEndDate, + true, false); + } + + private IPage> candidateDetails(IPage page, Long contractId, String settlementType, + String batchNo, String startDate, String endDate, boolean byCreateTime, + boolean validateContractType) { if (contractId == null) { throw new ServiceException("请先选择合同"); } validateSettlementType(settlementType); ContractManage contract = loadAvailableContract(contractId); - if (!Objects.equals(contractSettlementType(contract), settlementType)) { + // 正式结算需兼容历史单据及预结算转正式结算时保留的结算类型, + // 候选数据仍由合同、项目、组织、结算类型及未结算状态共同约束。 + if (validateContractType && !Objects.equals(contractSettlementType(contract), settlementType)) { throw new ServiceException("结算类型与合同类别不一致"); } LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() @@ -223,8 +240,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl query.eq(ReceivablePayableDetail::getCustomerName, contract.getPartyA()) .or().eq(ReceivablePayableDetail::getCustomerName, contract.getPartyB())) .like(Func.isNotEmpty(batchNo), ReceivablePayableDetail::getBatchNo, batchNo) - .ge(Func.isNotEmpty(feeStartDate), ReceivablePayableDetail::getFeeDate, parseDate(feeStartDate)) - .le(Func.isNotEmpty(feeEndDate), ReceivablePayableDetail::getFeeDate, parseDate(feeEndDate)) + .ge(!byCreateTime && Func.isNotEmpty(startDate), ReceivablePayableDetail::getFeeDate, + parseDate(startDate)) + .le(!byCreateTime && Func.isNotEmpty(endDate), ReceivablePayableDetail::getFeeDate, + parseDate(endDate)) + .ge(byCreateTime && Func.isNotEmpty(startDate), ReceivablePayableDetail::getCreateTime, + byCreateTime && Func.isNotEmpty(startDate) ? parseDate(startDate).atStartOfDay() : null) + .lt(byCreateTime && Func.isNotEmpty(endDate), ReceivablePayableDetail::getCreateTime, + byCreateTime && Func.isNotEmpty(endDate) ? parseDate(endDate).plusDays(1).atStartOfDay() : null) .orderByDesc(ReceivablePayableDetail::getCreateTime); IPage sourcePage = sourceDetailMapper.selectPage( new Page<>(page.getCurrent(), page.getSize()), wrapper); @@ -613,30 +636,46 @@ public class PreSettlementServiceImpl extends BaseServiceImpl candidateMap(ReceivablePayableDetail source) { Map result = new LinkedHashMap<>(); + Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId()); result.put("id", source.getId()); result.put("documentNo", source.getDocumentNo()); + result.put("createTime", source.getCreateTime()); + result.put("projectName", source.getProjectName()); + result.put("deptName", source.getDeptName()); result.put("feeDate", source.getFeeDate()); result.put("customerName", source.getCustomerName()); - result.put("departureAddress", waybillValue(source.getWaybillId(), Waybill::getDepartureAddress)); - result.put("arrivalAddress", waybillValue(source.getWaybillId(), Waybill::getArrivalAddress)); + result.put("contractNo", source.getContractNo()); result.put("contractName", source.getContractName()); + result.put("sourceType", source.getSourceType()); + result.put("preSettlementNo", source.getPreSettlementNo()); + result.put("formalSettlementNo", source.getFormalSettlementNo()); result.put("waybillNo", source.getWaybillNo()); result.put("vehicleNo", source.getVehicleNo()); + result.put("departureAddress", waybill == null ? "" : + firstNotEmpty(waybill.getDepartureAddress(), waybill.getDepartureName())); + result.put("arrivalAddress", waybill == null ? "" : + firstNotEmpty(waybill.getArrivalAddress(), waybill.getArrivalName())); + result.put("actualDepartureTime", waybill == null || waybill.getStartDate() == null ? null : + waybill.getStartDate().atStartOfDay()); + result.put("actualCompletionTime", waybill == null || waybill.getEndDate() == null ? null : + waybill.getEndDate().atStartOfDay()); result.put("transportType", source.getTransportType()); result.put("cargoName", source.getCargoName()); result.put("cargoType", source.getCargoType()); + result.put("transportQuantity", source.getTransportQuantity()); + result.put("quantityUnit", source.getQuantityUnit()); + result.put("mileage", source.getMileage()); result.put("batchNo", source.getBatchNo()); + result.put("unitPrice", source.getUnitPrice()); + result.put("freightAmount", money(source.getFreightAmount())); + result.put("feeItemsJson", source.getFeeItemsJson()); result.put("totalAmount", money(source.getTotalAmount())); result.put("currency", source.getCurrency()); + result.put("settlementStatusName", "待结算"); + result.put("remark", source.getRemark()); return result; } - private String waybillValue(Long waybillId, Function getter) { - if (waybillId == null) return ""; - Waybill waybill = waybillService.getById(waybillId); - return waybill == null ? "" : getter.apply(waybill); - } - private void synchronizeDetails(PreSettlement settlement, List requestedIds, boolean allowSourceMismatch) { List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); List existingDetails = listDetails(settlement.getId()); diff --git a/doc/sql/transport/blade_common_route_region_id_patch_20260823.sql b/doc/sql/transport/blade_common_route_region_id_patch_20260823.sql new file mode 100644 index 0000000..7f2ddb8 --- /dev/null +++ b/doc/sql/transport/blade_common_route_region_id_patch_20260823.sql @@ -0,0 +1,7 @@ +ALTER TABLE `blade_common_route` + ADD COLUMN `departure_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货省ID' AFTER `departure_name`, + ADD COLUMN `departure_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货市ID' AFTER `departure_province_id`, + ADD COLUMN `departure_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货区ID' AFTER `departure_city_id`, + ADD COLUMN `arrival_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货省ID' AFTER `arrival_name`, + ADD COLUMN `arrival_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货市ID' AFTER `arrival_province_id`, + ADD COLUMN `arrival_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货区ID' AFTER `arrival_city_id`; diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql index bc05c11..ab73396 100644 --- a/doc/sql/transport/blade_formal_settlement_20260818.sql +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -62,6 +62,22 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_detail_fee` ( KEY `idx_formal_detail_fee_detail` (`formal_settlement_detail_id`), KEY `idx_formal_detail_fee_source` (`source_fee_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算货物费用快照'; +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_summary_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', `fee_type` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型', + `fee_item` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `remark` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `manual_flag` int(11) NOT NULL DEFAULT '0' COMMENT '是否手工添加', PRIMARY KEY (`id`) USING BTREE, + KEY `idx_formal_settlement_summary_bill` (`formal_settlement_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算合计费用'; + CREATE TABLE IF NOT EXISTS `blade_formal_settlement_payment` ( `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, diff --git a/doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql b/doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql new file mode 100644 index 0000000..9f96b53 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_summary_fee_20260823.sql @@ -0,0 +1,24 @@ +-- 正式结算新增结算合计费用表 + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_summary_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `fee_type` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型', + `fee_item` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `remark` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `manual_flag` int(11) NOT NULL DEFAULT '0' COMMENT '是否手工添加', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_formal_settlement_summary_bill` (`formal_settlement_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算合计费用'; diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index f6f44c8..a892e3c 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -18,6 +18,9 @@ CREATE TABLE `blade_common_route` ( `route_name` varchar(100) DEFAULT NULL COMMENT '线路名称', `departure_address_id` bigint(20) DEFAULT NULL COMMENT '发货地址ID', `departure_name` varchar(100) DEFAULT NULL COMMENT '发货地', + `departure_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货省ID', + `departure_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货市ID', + `departure_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货区ID', `departure_address` varchar(255) DEFAULT NULL COMMENT '发货地址', `departure_longitude` decimal(18,6) DEFAULT NULL COMMENT '发货经度', `departure_latitude` decimal(18,6) DEFAULT NULL COMMENT '发货纬度', @@ -25,6 +28,9 @@ CREATE TABLE `blade_common_route` ( `departure_phone` varchar(100) DEFAULT NULL COMMENT '发货联系方式', `arrival_address_id` bigint(20) DEFAULT NULL COMMENT '收货地址ID', `arrival_name` varchar(100) DEFAULT NULL COMMENT '收货地', + `arrival_province_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货省ID', + `arrival_city_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货市ID', + `arrival_district_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收货区ID', `arrival_address` varchar(255) DEFAULT NULL COMMENT '收货地址', `arrival_longitude` decimal(18,6) DEFAULT NULL COMMENT '收货经度', `arrival_latitude` decimal(18,6) DEFAULT NULL COMMENT '收货纬度',