From df6d417e40d223fe9233687227072cd184b08c5f Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Wed, 19 Aug 2026 06:53:39 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=8D=95=202=E3=80=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BF=90=E8=BE=93=E5=AF=B9=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/ApiScopePermissionHandler.java | 4 + .../pojo/dto/PreSettlementSaveRequest.java | 6 + .../dto/SettlementAdjustmentSaveRequest.java | 28 + .../SettlementAdjustmentStatusRequest.java | 12 + ...sportReconciliationManualMatchRequest.java | 15 + .../TransportReconciliationSaveRequest.java | 18 + .../pojo/entity/SettlementAdjustment.java | 35 ++ .../entity/SettlementAdjustmentDetail.java | 25 + .../pojo/entity/TransportReconciliation.java | 68 +++ .../TransportReconciliationChangeRecord.java | 39 ++ .../TransportReconciliationExternal.java | 49 ++ .../TransportReconciliationInternal.java | 55 ++ .../pojo/vo/SettlementAdjustmentVO.java | 22 + .../pojo/vo/TransportReconciliationVO.java | 28 + .../service/ISettlementAdjustmentService.java | 23 + .../ReceivablePayableDetailController.java | 9 +- .../SettlementAdjustmentController.java | 36 ++ .../TransportReconciliationController.java | 144 +++++ .../excel/CargoReconciliationExcel.java | 41 ++ .../CargoReconciliationFailureExcel.java | 13 + .../excel/VehicleReconciliationExcel.java | 40 ++ .../VehicleReconciliationFailureExcel.java | 13 + .../SettlementAdjustmentDetailMapper.java | 8 + .../mapper/SettlementAdjustmentMapper.java | 8 + ...sportReconciliationChangeRecordMapper.java | 11 + ...TransportReconciliationExternalMapper.java | 11 + ...TransportReconciliationInternalMapper.java | 11 + .../mapper/TransportReconciliationMapper.java | 11 + .../IReceivablePayableDetailService.java | 3 +- .../ITransportReconciliationService.java | 39 ++ .../impl/PreSettlementServiceImpl.java | 19 +- .../ReceivablePayableDetailServiceImpl.java | 125 +++- .../impl/SettlementAdjustmentServiceImpl.java | 205 +++++++ .../TransportReconciliationServiceImpl.java | 561 ++++++++++++++++++ .../TransportReconciliationWrapper.java | 30 + .../blade_settlement_adjustment_20260818.sql | 39 ++ ...lade_transport_reconciliation_20260818.sql | 82 +++ 37 files changed, 1843 insertions(+), 43 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java create mode 100644 doc/sql/transport/blade_settlement_adjustment_20260818.sql create mode 100644 doc/sql/transport/blade_transport_reconciliation_20260818.sql diff --git a/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java b/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java index 646bbd9..18e2227 100644 --- a/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java +++ b/blade-service-api/blade-scope-api/src/main/java/org/springblade/system/handler/ApiScopePermissionHandler.java @@ -75,6 +75,10 @@ public class ApiScopePermissionHandler implements IPermissionHandler { if (request == null || user == null) { return false; } + // 超级管理员在菜单授权树中默认拥有全部菜单权限,与框架默认处理器保持一致。 + if (AuthUtil.isAdministrator()) { + return true; + } List codes = permissionMenu(permission, user.getRoleId()); return codes != null && !codes.isEmpty(); } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java index 1140702..d892003 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java @@ -52,6 +52,9 @@ public class PreSettlementSaveRequest implements Serializable { @Schema(description = "合同ID") private Long contractId; + @Schema(description = "结算类型:receivable/payable") + private String settlementType; + @Schema(description = "汇率日期") private LocalDate exchangeRateDate; @@ -67,6 +70,9 @@ public class PreSettlementSaveRequest implements Serializable { @Schema(description = "应收应付明细ID") private List sourceDetailIds; + @Schema(description = "是否允许批量转结算兼容历史来源明细的项目、所属组织或客商差异") + private Boolean allowSourceMismatch; + @Schema(description = "结算合计费用") private List summaryFees; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java new file mode 100644 index 0000000..bf7577f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentSaveRequest.java @@ -0,0 +1,28 @@ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Data +public class SettlementAdjustmentSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long formalSettlementId; + private String remark; + private List details; + + @Data + public static class Detail implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementDetailId; + private Long formalSettlementDetailFeeId; + private String feeType; + private String feeItem; + private BigDecimal adjustmentAmountTax; + private BigDecimal adjustmentAmountNoTax; + private String remark; + } +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java new file mode 100644 index 0000000..981293b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/SettlementAdjustmentStatusRequest.java @@ -0,0 +1,12 @@ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +@Data +public class SettlementAdjustmentStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java new file mode 100644 index 0000000..c29dd0a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationManualMatchRequest.java @@ -0,0 +1,15 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** 运输对账人工匹配请求。 @author Chill */ +@Data +public class TransportReconciliationManualMatchRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Long internalId; + private Long externalId; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java new file mode 100644 index 0000000..be1bcb3 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/TransportReconciliationSaveRequest.java @@ -0,0 +1,18 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDate; + +/** 运输对账单保存请求。 @author Chill */ +@Data +public class TransportReconciliationSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long formalSettlementId; + private String reconciliationMode; + private LocalDate reconciliationDate; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java new file mode 100644 index 0000000..08e5977 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java @@ -0,0 +1,35 @@ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_settlement_adjustment") +public class SettlementAdjustment extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String adjustmentNo; + private Long formalSettlementId; + private String formalSettlementNo; + private String settlementType; + private String projectName; + private String deptName; + private String customerName; + private String contractNo; + private String contractName; + private BigDecimal adjustmentAmount; + private BigDecimal originalSettlementAmount; + private BigDecimal adjustedSettlementAmount; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeSyncStatus; + private String remark; + private LocalDateTime approvedTime; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java new file mode 100644 index 0000000..bda96fa --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustmentDetail.java @@ -0,0 +1,25 @@ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_settlement_adjustment_detail") +public class SettlementAdjustmentDetail extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long adjustmentId; + private Long formalSettlementDetailId; + private Long formalSettlementDetailFeeId; + private String feeType; + private String feeItem; + private BigDecimal originalAmountTax; + private BigDecimal adjustmentAmountTax; + private BigDecimal adjustmentAmountNoTax; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java new file mode 100644 index 0000000..ee00039 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliation.java @@ -0,0 +1,68 @@ +/** + * 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; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 运输对账单实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation") +@Schema(description = "运输对账单") +public class TransportReconciliation extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String reconciliationNo; + private Long formalSettlementId; + private String formalSettlementNo; + private String preSettlementNos; + private String settlementType; + private String reconciliationMode; + private Long projectId; + private String projectName; + private Long deptId; + private String deptName; + private Long contractId; + private String contractNo; + private String contractName; + private String payerName; + private String payeeName; + private String currency; + private BigDecimal settlementAmount; + private BigDecimal paidAmount; + private Long reconcilerId; + private String reconcilerName; + private LocalDate reconciliationDate; + private String reconciliationStatus; + private String matchStatus; + private Integer internalBillCount; + private Integer externalBillCount; + private Integer differenceCount; + private BigDecimal internalQuantity; + private BigDecimal externalQuantity; + private BigDecimal differenceQuantity; + private BigDecimal internalAmount; + private BigDecimal externalAmount; + private BigDecimal differenceAmount; + private Integer matchedCount; + private Integer unmatchedCount; + private Boolean billUpdated; + private LocalDateTime completedTime; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java new file mode 100644 index 0000000..dd2618b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationChangeRecord.java @@ -0,0 +1,39 @@ +/** + * 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 lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** 运输对账账单变更记录。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation_change_record") +public class TransportReconciliationChangeRecord extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Long internalDetailId; + private Long formalSettlementId; + private Long formalSettlementDetailId; + private Long sourceDetailId; + private String documentNo; + private String cargoName; + private BigDecimal beforeAmount; + private BigDecimal afterAmount; + private String beforeDataJson; + private String afterDataJson; + private Long operatorId; + private String operatorName; + private LocalDateTime changeTime; + private String changeReason; +} 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 new file mode 100644 index 0000000..353febe --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationExternal.java @@ -0,0 +1,49 @@ +/** + * 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 lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** 运输对账外部账单行。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation_external") +public class TransportReconciliationExternal extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Integer externalLineNo; + private String vehicleNo; + private String departureAddress; + private String arrivalAddress; + private LocalDateTime actualDepartureTime; + private LocalDateTime actualCompletionTime; + private String transportType; + private String cargoName; + private String cargoType; + private String specification; + private String model; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal settlementAmount; + private Boolean suspectedDuplicate; + private String matchStatus; + private Long matchedInternalId; + private String errorMessage; + private String rawDataJson; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java new file mode 100644 index 0000000..4ea0058 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportReconciliationInternal.java @@ -0,0 +1,55 @@ +/** + * 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 lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** 运输对账内部账单快照。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_transport_reconciliation_internal") +public class TransportReconciliationInternal extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long reconciliationId; + private Long formalSettlementDetailId; + private Long formalSettlementDetailFeeId; + private Long sourceDetailId; + private Long sourceCargoFeeId; + private Integer lineNo; + private String documentNo; + private String waybillNo; + private String vehicleNo; + private String departureAddress; + private String arrivalAddress; + private LocalDateTime actualDepartureTime; + private LocalDateTime actualCompletionTime; + private String transportType; + private String cargoName; + private String cargoType; + private String specification; + private String model; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal settlementAmount; + private Long matchedExternalId; + private Integer matchedExternalLineNo; + private String matchResult; + private String updateResult; + private String updateMessage; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java new file mode 100644 index 0000000..3f00b3a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java @@ -0,0 +1,22 @@ +package org.springblade.transport.pojo.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; + +import java.time.LocalDate; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SettlementAdjustmentVO extends SettlementAdjustment { + private LocalDate createStartDate; + private LocalDate createEndDate; + private String approvalStatusName; + private String settlementTypeName; + private String createUserName; + private List details; + private List formalDetails; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java new file mode 100644 index 0000000..5b7ebda --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TransportReconciliationVO.java @@ -0,0 +1,28 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.pojo.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; +import org.springblade.transport.pojo.entity.TransportReconciliationExternal; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; + +import java.io.Serial; +import java.util.List; + +/** 运输对账单视图实体类。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TransportReconciliationVO extends TransportReconciliation { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String updateUserName; + @TableField(exist = false) private String reconciliationModeName; + @TableField(exist = false) private String reconciliationStatusName; + @TableField(exist = false) private String matchStatusName; + @TableField(exist = false) private List internalDetails; + @TableField(exist = false) private List externalDetails; + @TableField(exist = false) private List changeRecords; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java new file mode 100644 index 0000000..aa4daff --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/service/ISettlementAdjustmentService.java @@ -0,0 +1,23 @@ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; +import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; + +import java.util.List; +import java.util.Map; + +public interface ISettlementAdjustmentService { + IPage selectPage(IPage page, SettlementAdjustmentVO query); + SettlementAdjustmentVO detail(Long id); + List> candidateFormalSettlements(String keyword); + List> formalDetails(Long formalSettlementId); + Long saveDraft(SettlementAdjustmentSaveRequest request); + void removeDraft(Long id); + void submit(SettlementAdjustmentStatusRequest request); + void approve(SettlementAdjustmentStatusRequest request); + void returnBill(SettlementAdjustmentStatusRequest request); + String repush(Long adjustmentId); +} 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 dfa2720..c0bd427 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 @@ -118,11 +118,12 @@ public class ReceivablePayableDetailController extends BladeController { public R>> transferCandidates(Query query, @RequestParam(required = false) String contractName, @RequestParam(required = false) String batchNo, - @RequestParam(required = false) String generateStartDate, - @RequestParam(required = false) String generateEndDate, - @RequestParam(required = false) String settlementBillType) { + @RequestParam(required = false) String generateStartDate, + @RequestParam(required = false) String generateEndDate, + @RequestParam(required = false) String settlementBillType, + @RequestParam(required = false) String settlementType) { return R.data(detailService.transferCandidates(Condition.getPage(query), contractName, batchNo, - generateStartDate, generateEndDate, settlementBillType)); + generateStartDate, generateEndDate, settlementBillType, settlementType)); } @PostMapping("/transfer-settlement") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java new file mode 100644 index 0000000..497ed40 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/SettlementAdjustmentController.java @@ -0,0 +1,36 @@ +package org.springblade.transport.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; +import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; +import org.springblade.transport.pojo.entity.SettlementAdjustment; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; +import org.springblade.transport.service.ISettlementAdjustmentService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@AllArgsConstructor +@PreAuth(menu = "settlement_adjustment") +@RequestMapping("/settlement-adjustment") +public class SettlementAdjustmentController extends BladeController { + private final ISettlementAdjustmentService service; + @GetMapping("/list") public R> list(SettlementAdjustmentVO query, Query page) { return R.data(service.selectPage(Condition.getPage(page), query)); } + @GetMapping("/detail") public R detail(@RequestParam Long id) { return R.data(service.detail(id)); } + @GetMapping("/candidate-formal-settlements") public R>> candidates(@RequestParam(required = false) String keyword) { return R.data(service.candidateFormalSettlements(keyword)); } + @GetMapping("/formal-details") public R>> formalDetails(@RequestParam Long formalSettlementId) { return R.data(service.formalDetails(formalSettlementId)); } + @PostMapping("/save") public R save(@RequestBody SettlementAdjustmentSaveRequest request) { return R.data(service.saveDraft(request)); } + @PostMapping("/remove") public R remove(@RequestParam Long id) { service.removeDraft(id); return R.success("删除成功"); } + @PostMapping("/submit") public R submit(@RequestBody SettlementAdjustmentStatusRequest request) { service.submit(request); return R.success("提交成功"); } + @PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) { service.approve(request); return R.success("审批通过"); } + @PostMapping("/return") public R returnBill(@RequestBody SettlementAdjustmentStatusRequest request) { service.returnBill(request); return R.success("已驳回"); } + @PostMapping("/repush") public R repush(@RequestParam Long id) { return R.data(service.repush(id)); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java new file mode 100644 index 0000000..7ca7b65 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportReconciliationController.java @@ -0,0 +1,144 @@ +/** + * 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.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.common.excel.ImportFailureExcelUtil; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.excel.util.ExcelUtil; +import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFailureExcel; +import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; +import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; +import org.springblade.transport.service.ITransportReconciliationService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +/** 运输对账单控制器。 @author Chill */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "transport_reconciliation") +@RequestMapping("/transport-reconciliation") +@Tag(name = "运输对账", description = "运输对账管理") +public class TransportReconciliationController extends BladeController { + private final ITransportReconciliationService reconciliationService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "运输对账分页") + public R> list(TransportReconciliationVO query, Query pageQuery) { + return R.data(reconciliationService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "运输对账详情") + public R detail(@RequestParam Long id) { return R.data(reconciliationService.detail(id)); } + + @GetMapping("/formal-options") + @ApiOperationSupport(order = 3) + @Operation(summary = "可选正式结算单") + public R> formalOptions(Query pageQuery, @RequestParam(required = false) String settlementType, + @RequestParam(required = false) String keyword) { + return R.data(reconciliationService.formalOptions(Condition.getPage(pageQuery), settlementType, keyword)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 4) + @Operation(summary = "保存运输对账草稿") + public R save(@RequestBody TransportReconciliationSaveRequest request) { return R.data(reconciliationService.saveDraft(request)); } + + @PostMapping("/remove") + @ApiOperationSupport(order = 5) + @Operation(summary = "删除运输对账草稿") + public R remove(@RequestParam Long id) { reconciliationService.removeDraft(id); return R.success("删除成功"); } + + @PostMapping("/import-vehicle") + @ApiOperationSupport(order = 6) + @Operation(summary = "导入整车总额外部账单") + public R importVehicle(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { + List failures = reconciliationService.importVehicles(id, ExcelUtil.read(file, VehicleReconciliationExcel.class)); + if (!failures.isEmpty()) { + ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, VehicleReconciliationFailureExcel.class); + return null; + } + return R.success("导入数据成功"); + } + + @PostMapping("/import-cargo") + @ApiOperationSupport(order = 7) + @Operation(summary = "导入货物明细外部账单") + public R importCargo(@RequestParam Long id, MultipartFile file, HttpServletResponse response) { + List failures = reconciliationService.importCargoes(id, ExcelUtil.read(file, CargoReconciliationExcel.class)); + if (!failures.isEmpty()) { + ImportFailureExcelUtil.export(response, "运输对账导入失败明细" + DateUtil.time(), "导入失败明细", failures, CargoReconciliationFailureExcel.class); + return null; + } + return R.success("导入数据成功"); + } + + @GetMapping("/template") + @ApiOperationSupport(order = 8) + @Operation(summary = "下载运输对账模板") + public void template(@RequestParam String mode, HttpServletResponse response) { + if ("cargo".equals(mode)) ExcelUtil.export(response, "货物明细对账模板", "货物明细对账模板", new ArrayList(), CargoReconciliationExcel.class); + else ExcelUtil.export(response, "整车总额对账模板", "整车总额对账模板", new ArrayList(), VehicleReconciliationExcel.class); + } + + @PostMapping("/match") + @ApiOperationSupport(order = 9) + @Operation(summary = "自动匹配内部账单") + public R match(@RequestParam Long id) { reconciliationService.autoMatch(id); return R.success("匹配完成"); } + + @PostMapping("/manual-match") + @ApiOperationSupport(order = 10) + @Operation(summary = "人工匹配账单明细") + public R manualMatch(@RequestBody TransportReconciliationManualMatchRequest request) { reconciliationService.manualMatch(request); return R.success("人工匹配成功"); } + + @PostMapping("/unmatch") + @ApiOperationSupport(order = 11) + @Operation(summary = "取消明细匹配") + public R unmatch(@RequestParam Long internalId) { reconciliationService.unmatch(internalId); return R.success("已取消匹配"); } + + @PostMapping("/adjust") + @ApiOperationSupport(order = 12) + @Operation(summary = "调整内部账单明细") + public R adjust(@RequestBody TransportReconciliationInternal row) { reconciliationService.adjustInternal(row); return R.success("调整成功"); } + + @PostMapping("/update-by-match") + @ApiOperationSupport(order = 13) + @Operation(summary = "按匹配结果更新账单") + public R updateByMatch(@RequestParam Long id) { reconciliationService.updateByMatch(id); return R.success("账单更新完成"); } + + @PostMapping("/complete") + @ApiOperationSupport(order = 14) + @Operation(summary = "完成运输对账") + public R complete(@RequestParam Long id) { reconciliationService.complete(id); return R.success("对账单确认完成"); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java new file mode 100644 index 0000000..a8f55a8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationExcel.java @@ -0,0 +1,41 @@ +/** + * 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.excel; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.NumberFormat; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** 货物明细对账导入模型。 @author Chill */ +@Data +@ColumnWidth(22) +public class CargoReconciliationExcel implements Serializable { + @Serial private static final long serialVersionUID = 1L; + @ExcelProperty("车牌号") private String vehicleNo; + @ExcelProperty("发货地址") private String departureAddress; + @ExcelProperty("到货地址") private String arrivalAddress; + @ExcelProperty("实际发货时间") private String actualDepartureTime; + @ExcelProperty("实际完成时间") private String actualCompletionTime; + @ExcelProperty("货物名称") private String cargoName; + @ExcelProperty("货物类型") private String cargoType; + @ExcelProperty("规格") private String specification; + @ExcelProperty("型号") private String model; + @ExcelProperty("运输量") @NumberFormat("0.000000") private BigDecimal transportQuantity; + @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; + @ExcelProperty("里程(KM)") @NumberFormat("0.00") private BigDecimal mileage; + @ExcelProperty("运输费") @NumberFormat("0.00") private BigDecimal freightAmount; + @ExcelProperty("费用项目名称1") @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelProperty("费用项目名称2") @NumberFormat("0.00") private BigDecimal feeItemTwo; + @ExcelProperty("结算金额") @NumberFormat("0.00") private BigDecimal settlementAmount; + @ExcelIgnore private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java new file mode 100644 index 0000000..ffdcb7e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CargoReconciliationFailureExcel.java @@ -0,0 +1,13 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 货物明细对账导入失败模型。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CargoReconciliationFailureExcel extends CargoReconciliationExcel { + @ExcelProperty("导入失败原因") private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.java new file mode 100644 index 0000000..035d082 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationExcel.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.excel; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.NumberFormat; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** 整车总额对账导入模型。 @author Chill */ +@Data +@ColumnWidth(22) +public class VehicleReconciliationExcel implements Serializable { + @Serial private static final long serialVersionUID = 1L; + @ExcelProperty("车牌号") private String vehicleNo; + @ExcelProperty("发货地址") private String departureAddress; + @ExcelProperty("到货地址") private String arrivalAddress; + @ExcelProperty("实际发货时间") private String actualDepartureTime; + @ExcelProperty("实际完成时间") private String actualCompletionTime; + @ExcelProperty("运输类型") private String transportType; + @ExcelProperty("货物名称") private String cargoName; + @ExcelProperty("货物类型") private String cargoType; + @ExcelProperty("运输总量") @NumberFormat("0.000000") private BigDecimal transportQuantity; + @ExcelProperty("里程(KM)") @NumberFormat("0.00") private BigDecimal mileage; + @ExcelProperty("批次号") private String batchNo; + @ExcelProperty("运输单价") @NumberFormat("0.00") private BigDecimal unitPrice; + @ExcelProperty("运费") @NumberFormat("0.00") private BigDecimal freightAmount; + @ExcelProperty("费用项目1") @NumberFormat("0.00") private BigDecimal feeItemOne; + @ExcelProperty("结算费用合计") @NumberFormat("0.00") private BigDecimal settlementAmount; + @ExcelIgnore private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java new file mode 100644 index 0000000..5d77df1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/VehicleReconciliationFailureExcel.java @@ -0,0 +1,13 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 整车对账导入失败模型。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +public class VehicleReconciliationFailureExcel extends VehicleReconciliationExcel { + @ExcelProperty("导入失败原因") private String errorMessage; +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java new file mode 100644 index 0000000..b0e3ba2 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentDetailMapper.java @@ -0,0 +1,8 @@ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; + +@Mapper +public interface SettlementAdjustmentDetailMapper extends BaseMapper {} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java new file mode 100644 index 0000000..1a2b562 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/SettlementAdjustmentMapper.java @@ -0,0 +1,8 @@ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.SettlementAdjustment; + +@Mapper +public interface SettlementAdjustmentMapper extends BaseMapper {} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java new file mode 100644 index 0000000..dad962f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationChangeRecordMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; + +/** 运输对账变更记录 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationChangeRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java new file mode 100644 index 0000000..70d9fde --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationExternalMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliationExternal; + +/** 运输对账外部账单 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationExternalMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java new file mode 100644 index 0000000..76049c5 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationInternalMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; + +/** 运输对账内部账单 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationInternalMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java new file mode 100644 index 0000000..1a664e5 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportReconciliationMapper.java @@ -0,0 +1,11 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.TransportReconciliation; + +/** 运输对账单 Mapper。 @author Chill */ +@Mapper +public interface TransportReconciliationMapper extends BaseMapper { +} 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 a500178..bb9aa65 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 @@ -58,7 +58,8 @@ public interface IReceivablePayableDetailService extends BaseService> transferCandidates(IPage page, String contractName, String batchNo, - String generateStartDate, String generateEndDate, String settlementBillType); + String generateStartDate, String generateEndDate, String settlementBillType, + String settlementType); IPage> generateWaybills(IPage page, ReceivablePayableGenerateRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java new file mode 100644 index 0000000..a31fca8 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ITransportReconciliationService.java @@ -0,0 +1,39 @@ +/** + * 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.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFailureExcel; +import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; +import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; + +import java.util.List; + +/** 运输对账单服务。 @author Chill */ +public interface ITransportReconciliationService extends BaseService { + IPage selectPage(IPage page, TransportReconciliationVO query); + IPage formalOptions(IPage page, String settlementType, String keyword); + TransportReconciliationVO detail(Long id); + Long saveDraft(TransportReconciliationSaveRequest request); + void removeDraft(Long id); + List importVehicles(Long id, List rows); + List importCargoes(Long id, List rows); + void autoMatch(Long id); + void manualMatch(TransportReconciliationManualMatchRequest request); + void unmatch(Long internalId); + void adjustInternal(TransportReconciliationInternal row); + void updateByMatch(Long id); + void complete(Long id); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index e90abaf..55fc4b1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -272,7 +272,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl requestedIds) { + private void synchronizeDetails(PreSettlement settlement, List requestedIds, boolean allowSourceMismatch) { List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); List existingDetails = listDetails(settlement.getId()); Set requestedSet = new LinkedHashSet<>(distinctIds); @@ -661,7 +661,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl requestedSet.contains(detail.getSourceDetailId())); for (ReceivablePayableDetail source : sources) { - validateCandidate(settlement, source); + validateCandidate(settlement, source, allowSourceMismatch); String sourceCurrency = Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency(); if (!hasRetainedDetail) { settlement.setCurrency(sourceCurrency); @@ -691,16 +691,17 @@ public class PreSettlementServiceImpl extends BaseServiceImpl detailIds = details.stream().map(ReceivablePayableDetail::getId).toList(); + ReceivablePayableDetail first = details.get(0); + if ("pre".equals(request.getSettlementBillType())) { + PreSettlementSaveRequest saveRequest = new PreSettlementSaveRequest(); + saveRequest.setContractId(first.getContractId()); + saveRequest.setSettlementType(first.getSettlementType()); + saveRequest.setSourceDetailIds(detailIds); + saveRequest.setAllowSourceMismatch(true); + preSettlementService.saveDraft(saveRequest); + return; + } + FormalSettlementSaveRequest saveRequest = new FormalSettlementSaveRequest(); + saveRequest.setContractId(first.getContractId()); + saveRequest.setSettlementType(first.getSettlementType()); + saveRequest.setSourceDetailIds(detailIds); + formalSettlementService.saveDraft(saveRequest); + } + + private void validateTransferDetails(List details) { + ReceivablePayableDetail first = details.get(0); + String settlementType = first.getSettlementType(); + Long contractId = first.getContractId(); + if (Func.isEmpty(settlementType) || contractId == null + || details.stream().anyMatch(detail -> Objects.equals(detail.getIsDeleted(), 1) + || !Objects.equals(detail.getSettlementType(), settlementType) + || !Objects.equals(detail.getContractId(), contractId) + || !"pending".equals(detail.getSettlementStatus()) + || Func.isNotEmpty(detail.getPreSettlementNo()) + || Func.isNotEmpty(detail.getFormalSettlementNo()))) { + throw new ServiceException("所选明细必须属于同一合同、结算类型且均为未结算状态"); } } @Override public IPage> transferCandidates(IPage page, String contractName, String batchNo, - String generateStartDate, String generateEndDate, String settlementBillType) { + String generateStartDate, String generateEndDate, String settlementBillType, + String settlementType) { ReceivablePayableDetailVO query = new ReceivablePayableDetailVO(); query.setContractName(contractName); query.setBatchNo(batchNo); query.setSettlementStatus("pending"); + query.setSettlementType(Func.isEmpty(settlementType) ? null : settlementType(settlementType)); query.setGenerateStartDate(parseDate(generateStartDate)); query.setGenerateEndDate(parseDate(generateEndDate)); - IPage detailPage = selectPage(new Page<>(page.getCurrent(), page.getSize()), query); + LambdaQueryWrapper wrapper = buildQuery(query) + .and(item -> item.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(item -> item.isNull(ReceivablePayableDetail::getFormalSettlementNo) + .or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")); + IPage detailPage = ReceivablePayableDetailWrapper.build() + .pageVO(page(new Page<>(page.getCurrent(), page.getSize()), wrapper)); Page> result = new Page<>(detailPage.getCurrent(), detailPage.getSize(), detailPage.getTotal()); - result.setRecords(detailPage.getRecords().stream().map(this::beanMap).toList()); + result.setRecords(detailPage.getRecords().stream().map(this::candidateMap).toList()); return result; } @@ -521,12 +558,12 @@ public class ReceivablePayableDetailServiceImpl BigDecimal total = fees.stream().map(ReceivablePayableCargoFee::getAfterAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal other = total.subtract(freight).setScale(2, RoundingMode.HALF_UP); ReceivablePayableDetail detail = new ReceivablePayableDetail(); - detail.setDocumentNo(nextDocumentNo()); + detail.setDocumentNo(nextDocumentNo(settlementType)); detail.setSettlementType(settlementType); - detail.setProjectId(waybill.getProjectId()); - detail.setProjectName(waybill.getProjectName()); - detail.setDeptId(waybill.getDeptId()); - detail.setDeptName(waybill.getDeptName()); + detail.setProjectId(contract == null ? waybill.getProjectId() : contract.getProjectId()); + detail.setProjectName(contract == null ? waybill.getProjectName() : contract.getProjectName()); + detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId()); + detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName()); detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate()); detail.setCustomerName("payable".equals(settlementType) ? (contract == null ? waybill.getCarrierName() : contract.getPartyA()) @@ -926,9 +963,36 @@ public class ReceivablePayableDetailServiceImpl return map; } - private Map beanMap(ReceivablePayableDetailVO detail) { + private Map candidateMap(ReceivablePayableDetailVO detail) { Map map = new LinkedHashMap<>(); - BeanUtil.copyProperties(detail, map); + map.put("id", detail.getId()); + map.put("documentNo", detail.getDocumentNo()); + map.put("settlementType", detail.getSettlementType()); + map.put("projectName", detail.getProjectName()); + map.put("deptName", detail.getDeptName()); + map.put("feeDate", detail.getFeeDate()); + map.put("customerName", detail.getCustomerName()); + map.put("contractNo", detail.getContractNo()); + map.put("contractName", detail.getContractName()); + map.put("preSettlementNo", detail.getPreSettlementNo()); + map.put("formalSettlementNo", detail.getFormalSettlementNo()); + map.put("waybillNo", detail.getWaybillNo()); + map.put("vehicleNo", detail.getVehicleNo()); + map.put("transportType", detail.getTransportType()); + map.put("cargoName", detail.getCargoName()); + map.put("cargoType", detail.getCargoType()); + map.put("transportQuantity", detail.getTransportQuantity()); + map.put("quantityUnit", detail.getQuantityUnit()); + map.put("mileage", detail.getMileage()); + map.put("batchNo", detail.getBatchNo()); + map.put("unitPrice", detail.getUnitPrice()); + map.put("currency", detail.getCurrency()); + map.put("freightAmount", detail.getFreightAmount()); + map.put("otherFeeAmount", detail.getOtherFeeAmount()); + map.put("totalAmount", detail.getTotalAmount()); + map.put("settlementStatus", detail.getSettlementStatus()); + map.put("settlementStatusName", detail.getSettlementStatusName()); + map.put("remark", detail.getRemark()); return map; } @@ -986,12 +1050,9 @@ public class ReceivablePayableDetailServiceImpl return Func.isEmpty(value) ? null : LocalDate.parse(value); } - private synchronized String nextDocumentNo() { - return "YS" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + System.currentTimeMillis() % 100000; - } - - private synchronized String settlementBillNo(String type) { - String prefix = "pre".equals(type) ? "YJ" : "ZJ"; + private synchronized String nextDocumentNo(String settlementType) { + String prefix = "payable".equals(settlementType) ? "YF" : "YS"; return prefix + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + System.currentTimeMillis() % 100000; } + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java new file mode 100644 index 0000000..761ce51 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -0,0 +1,205 @@ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.SettlementAdjustmentDetailMapper; +import org.springblade.transport.mapper.SettlementAdjustmentMapper; +import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest; +import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest; +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.SettlementAdjustment; +import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; +import org.springblade.transport.service.ISettlementAdjustmentService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class SettlementAdjustmentServiceImpl extends BaseServiceImpl + implements ISettlementAdjustmentService { + private static final String DRAFT = "draft"; + private static final String REVIEWING = "reviewing"; + private static final String APPROVED = "approved"; + private static final String RETURNED = "returned"; + private final SettlementAdjustmentDetailMapper detailMapper; + private final FormalSettlementMapper formalMapper; + private final FormalSettlementDetailMapper formalDetailMapper; + private final FormalSettlementDetailFeeMapper formalFeeMapper; + + @Override + public IPage selectPage(IPage page, SettlementAdjustmentVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getAdjustmentNo()), SettlementAdjustment::getAdjustmentNo, query.getAdjustmentNo()) + .like(Func.isNotEmpty(query.getFormalSettlementNo()), SettlementAdjustment::getFormalSettlementNo, query.getFormalSettlementNo()) + .like(Func.isNotEmpty(query.getCustomerName()), SettlementAdjustment::getCustomerName, query.getCustomerName()) + .like(Func.isNotEmpty(query.getProjectName()), SettlementAdjustment::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), SettlementAdjustment::getDeptName, query.getDeptName()) + .eq(Func.isNotEmpty(query.getSettlementType()), SettlementAdjustment::getSettlementType, query.getSettlementType()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), SettlementAdjustment::getApprovalStatus, query.getApprovalStatus()) + .ge(query.getCreateStartDate() != null, SettlementAdjustment::getCreateTime, + query.getCreateStartDate() == null ? null : query.getCreateStartDate().atStartOfDay()) + .lt(query.getCreateEndDate() != null, SettlementAdjustment::getCreateTime, + query.getCreateEndDate() == null ? null : query.getCreateEndDate().plusDays(1).atStartOfDay()) + .orderByDesc(SettlementAdjustment::getCreateTime); + return page(page, wrapper).convert(this::toVO); + } + + @Override + public SettlementAdjustmentVO detail(Long id) { + SettlementAdjustment adjustment = existing(id); + SettlementAdjustmentVO vo = toVO(adjustment); + vo.setDetails(detailMapper.selectList(Wrappers.lambdaQuery() + .eq(SettlementAdjustmentDetail::getAdjustmentId, id).orderByAsc(SettlementAdjustmentDetail::getCreateTime))); + vo.setFormalDetails(formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, adjustment.getFormalSettlementId()) + .orderByAsc(FormalSettlementDetail::getLineNo))); + return vo; + } + + @Override + public List> candidateFormalSettlements(String keyword) { + List rows = formalMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlement::getApprovalStatus, APPROVED) + .like(Func.isNotEmpty(keyword), FormalSettlement::getFormalSettlementNo, keyword) + .orderByDesc(FormalSettlement::getCreateTime)); + List> result = new ArrayList<>(); + for (FormalSettlement row : rows) { + Map item = new HashMap<>(); + item.put("id", row.getId()); item.put("formalSettlementNo", row.getFormalSettlementNo()); + item.put("settlementType", row.getSettlementType()); item.put("settlementTypeName", typeName(row.getSettlementType())); + item.put("projectName", row.getProjectName()); item.put("deptName", row.getDeptName()); + item.put("customerName", "receivable".equals(row.getSettlementType()) ? row.getPayerName() : row.getPayeeName()); + item.put("contractNo", row.getContractNo()); item.put("contractName", row.getContractName()); + item.put("settlementAmount", row.getSettlementAmount()); item.put("kingdeeSyncStatus", row.getKingdeeSyncStatus()); + result.add(item); + } + return result; + } + + @Override + public List> formalDetails(Long formalSettlementId) { + FormalSettlement settlement = formalMapper.selectById(formalSettlementId); + if (settlement == null || !APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单可调整"); + List> result = new ArrayList<>(); + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId).orderByAsc(FormalSettlementDetail::getLineNo)); + for (FormalSettlementDetail detail : details) { + for (FormalSettlementDetailFee fee : formalFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()).orderByAsc(FormalSettlementDetailFee::getLineNo))) { + Map item = new HashMap<>(); + item.put("formalSettlementDetailId", detail.getId()); item.put("formalSettlementDetailFeeId", fee.getId()); + item.put("documentNo", detail.getDocumentNo()); item.put("cargoName", fee.getCargoName()); + item.put("cargoType", fee.getCargoType()); item.put("feeType", Func.isEmpty(fee.getCargoType()) ? "运输费用" : fee.getCargoType()); + item.put("feeItem", Func.isEmpty(fee.getCargoName()) ? "结算调整" : fee.getCargoName()); item.put("originalAmountTax", fee.getSettlementAmountTax()); + item.put("remark", fee.getRemark()); result.add(item); + } + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(SettlementAdjustmentSaveRequest request) { + if (request.getFormalSettlementId() == null) throw new ServiceException("请选择关联正式结算单"); + if (Func.isEmpty(request.getDetails())) throw new ServiceException("请至少添加一条调整费用"); + FormalSettlement formal = formalMapper.selectById(request.getFormalSettlementId()); + if (formal == null || !APPROVED.equals(formal.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单可调整"); + SettlementAdjustment adjustment = request.getId() == null ? new SettlementAdjustment() : editable(request.getId()); + if (adjustment.getId() != null) detailMapper.delete(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId())); + if (adjustment.getId() == null) { adjustment.setAdjustmentNo(nextNo()); adjustment.setApprovalStatus(DRAFT); } + adjustment.setFormalSettlementId(formal.getId()); adjustment.setFormalSettlementNo(formal.getFormalSettlementNo()); + adjustment.setSettlementType(formal.getSettlementType()); adjustment.setProjectName(formal.getProjectName()); + adjustment.setDeptName(formal.getDeptName()); adjustment.setCustomerName("receivable".equals(formal.getSettlementType()) ? formal.getPayerName() : formal.getPayeeName()); + adjustment.setContractNo(formal.getContractNo()); adjustment.setContractName(formal.getContractName()); + adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); + adjustment.setOriginalSettlementAmount(money(formal.getSettlementAmount())); adjustment.setRemark(limit(request.getRemark(), 200)); + BigDecimal total = BigDecimal.ZERO; + if (request.getDetails() != null) for (SettlementAdjustmentSaveRequest.Detail row : request.getDetails()) { + FormalSettlementDetailFee fee = validateFee(formal.getId(), row.getFormalSettlementDetailId(), row.getFormalSettlementDetailFeeId()); + if (adjustment.getId() == null) save(adjustment); + SettlementAdjustmentDetail detail = new SettlementAdjustmentDetail(); detail.setAdjustmentId(adjustment.getId()); + detail.setFormalSettlementDetailId(row.getFormalSettlementDetailId()); detail.setFormalSettlementDetailFeeId(row.getFormalSettlementDetailFeeId()); + detail.setFeeType(row.getFeeType()); detail.setFeeItem(row.getFeeItem()); detail.setOriginalAmountTax(fee.getSettlementAmountTax()); + detail.setAdjustmentAmountTax(row.getAdjustmentAmountTax() == null ? BigDecimal.ZERO : row.getAdjustmentAmountTax()); + detail.setAdjustmentAmountNoTax(row.getAdjustmentAmountNoTax()); detail.setRemark(limit(row.getRemark(), 200)); detailMapper.insert(detail); + total = total.add(detail.getAdjustmentAmountTax()); + } + adjustment.setAdjustmentAmount(total); adjustment.setAdjustedSettlementAmount(adjustment.getOriginalSettlementAmount().add(total)); saveOrUpdate(adjustment); + return adjustment.getId(); + } + + @Override @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { SettlementAdjustment item = editable(id); detailMapper.delete(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, id)); removeById(item.getId()); } + @Override public void submit(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), DRAFT, REVIEWING, "审批中", null); } + @Override public void returnBill(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", limit(request.getReason(), 200)); } + + @Override + @Transactional(rollbackFor = Exception.class) + public String repush(Long adjustmentId) { + SettlementAdjustment adjustment = existing(adjustmentId); + if (!APPROVED.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批通过的结算调整单允许重新推送"); + FormalSettlement formal = formalMapper.selectById(adjustment.getFormalSettlementId()); + if (formal == null || !"synced".equals(formal.getKingdeeSyncStatus())) throw new ServiceException("关联正式结算单尚未推送金蝶,无需重新推送"); + String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + formal.setKingdeeBillNo(kingdeeNo); formal.setSyncedTime(LocalDateTime.now()); formalMapper.updateById(formal); + return kingdeeNo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(SettlementAdjustmentStatusRequest request) { + SettlementAdjustment adjustment = existing(request.getId()); + if (!REVIEWING.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批中的结算调整单允许审核"); + FormalSettlement formal = formalMapper.selectById(adjustment.getFormalSettlementId()); + if (formal == null || !APPROVED.equals(formal.getApprovalStatus())) throw new ServiceException("关联正式结算单状态已变化,无法审批"); + for (SettlementAdjustmentDetail item : detailMapper.selectList(Wrappers.lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId()))) { + FormalSettlementDetailFee fee = validateFee(formal.getId(), item.getFormalSettlementDetailId(), item.getFormalSettlementDetailFeeId()); + fee.setSettlementAmountTax(money(fee.getSettlementAmountTax()).add(money(item.getAdjustmentAmountTax()))); + if (item.getAdjustmentAmountNoTax() != null) fee.setSettlementAmountNoTax(money(fee.getSettlementAmountNoTax()).add(item.getAdjustmentAmountNoTax())); + fee.setAdjustAmount(money(fee.getAdjustAmount()).add(item.getAdjustmentAmountTax())); formalFeeMapper.updateById(fee); + } + for (FormalSettlementDetail detail : formalDetailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()))) { + List fees = formalFeeMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())); + detail.setSettlementAmountTax(fees.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setSettlementAmountNoTax(fees.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + } + BigDecimal amount = formalDetailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId())).stream().map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + formal.setSettlementAmount(amount); formal.setLocalSettlementAmount(amount.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); formalMapper.updateById(formal); + adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment); + } + + private void changeStatus(Long id, String from, String to, String node, String reason) { SettlementAdjustment item = existing(id); if (!from.equals(item.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); item.setApprovalStatus(to); item.setCurrentNode(node); if (reason != null) item.setRemark(reason); updateById(item); } + private SettlementAdjustment existing(Long id) { SettlementAdjustment item = getById(id); if (item == null || Objects.equals(item.getIsDeleted(), 1)) throw new ServiceException("结算调整单不存在"); return item; } + private SettlementAdjustment editable(Long id) { SettlementAdjustment item = existing(id); if (!(DRAFT.equals(item.getApprovalStatus()) || RETURNED.equals(item.getApprovalStatus()))) throw new ServiceException("仅草稿或驳回的调整单可编辑"); return item; } + private FormalSettlementDetailFee validateFee(Long formalId, Long detailId, Long feeId) { FormalSettlementDetail detail = formalDetailMapper.selectById(detailId); FormalSettlementDetailFee fee = formalFeeMapper.selectById(feeId); if (detail == null || fee == null || !Objects.equals(detail.getFormalSettlementId(), formalId) || !Objects.equals(fee.getFormalSettlementDetailId(), detailId)) throw new ServiceException("费用明细不存在或不属于关联正式结算单"); return fee; } + private SettlementAdjustmentVO toVO(SettlementAdjustment item) { SettlementAdjustmentVO vo = new SettlementAdjustmentVO(); org.springframework.beans.BeanUtils.copyProperties(item, vo); vo.setCreateUserName(UserCache.getUserRealName(item.getCreateUser())); vo.setApprovalStatusName(statusName(item.getApprovalStatus())); vo.setSettlementTypeName(typeName(item.getSettlementType())); return vo; } + private String statusName(String value) { return Map.of(DRAFT, "草稿", REVIEWING, "审批中", APPROVED, "审批通过", RETURNED, "已驳回").getOrDefault(value, value); } + private String typeName(String value) { return "receivable".equals(value) ? "应收" : "应付"; } + private synchronized String nextNo() { String prefix = "TZ" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = count(Wrappers.lambdaQuery().likeRight(SettlementAdjustment::getAdjustmentNo, prefix)); return prefix + String.format("%04d", count + 1); } + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("内容不能超过" + max + "个字"); return value; } +} 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 new file mode 100644 index 0000000..7d4a76f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportReconciliationServiceImpl.java @@ -0,0 +1,561 @@ +/** + * 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.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.transport.excel.CargoReconciliationExcel; +import org.springblade.transport.excel.CargoReconciliationFailureExcel; +import org.springblade.transport.excel.VehicleReconciliationExcel; +import org.springblade.transport.excel.VehicleReconciliationFailureExcel; +import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.FormalSettlementDetailMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableDetailMapper; +import org.springblade.transport.mapper.TransportReconciliationChangeRecordMapper; +import org.springblade.transport.mapper.TransportReconciliationExternalMapper; +import org.springblade.transport.mapper.TransportReconciliationInternalMapper; +import org.springblade.transport.mapper.TransportReconciliationMapper; +import org.springblade.transport.pojo.dto.TransportReconciliationManualMatchRequest; +import org.springblade.transport.pojo.dto.TransportReconciliationSaveRequest; +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.ReceivablePayableCargoFee; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.entity.TransportReconciliationChangeRecord; +import org.springblade.transport.pojo.entity.TransportReconciliationExternal; +import org.springblade.transport.pojo.entity.TransportReconciliationInternal; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; +import org.springblade.transport.service.ITransportReconciliationService; +import org.springblade.transport.wrapper.TransportReconciliationWrapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 运输对账单服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class TransportReconciliationServiceImpl + extends BaseServiceImpl + implements ITransportReconciliationService { + + private static final String VEHICLE = "vehicle"; + private static final String CARGO = "cargo"; + private static final String UNFINISHED = "unfinished"; + private static final String COMPLETED = "completed"; + private static final String MATCHED = "matched"; + private static final String UNMATCHED = "unmatched"; + private static final String DUPLICATE = "suspected_duplicate"; + private final FormalSettlementMapper formalSettlementMapper; + private final FormalSettlementSourceMapper formalSourceMapper; + private final FormalSettlementDetailMapper formalDetailMapper; + private final FormalSettlementDetailFeeMapper formalDetailFeeMapper; + private final ReceivablePayableDetailMapper receivablePayableMapper; + private final ReceivablePayableCargoFeeMapper cargoFeeMapper; + private final TransportReconciliationInternalMapper internalMapper; + private final TransportReconciliationExternalMapper externalMapper; + private final TransportReconciliationChangeRecordMapper changeRecordMapper; + + @Override + public IPage selectPage(IPage page, TransportReconciliationVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getReconciliationNo()), TransportReconciliation::getReconciliationNo, query.getReconciliationNo()) + .like(Func.isNotEmpty(query.getFormalSettlementNo()), TransportReconciliation::getFormalSettlementNo, query.getFormalSettlementNo()) + .like(Func.isNotEmpty(query.getPreSettlementNos()), TransportReconciliation::getPreSettlementNos, query.getPreSettlementNos()) + .like(Func.isNotEmpty(query.getProjectName()), TransportReconciliation::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), TransportReconciliation::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getContractNo()), TransportReconciliation::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getPayerName()), TransportReconciliation::getPayerName, query.getPayerName()) + .like(Func.isNotEmpty(query.getPayeeName()), TransportReconciliation::getPayeeName, query.getPayeeName()) + .eq(Func.isNotEmpty(query.getSettlementType()), TransportReconciliation::getSettlementType, query.getSettlementType()) + .eq(Func.isNotEmpty(query.getMatchStatus()), TransportReconciliation::getMatchStatus, query.getMatchStatus()) + .eq(Func.isNotEmpty(query.getReconciliationStatus()), TransportReconciliation::getReconciliationStatus, query.getReconciliationStatus()) + .orderByDesc(TransportReconciliation::getCreateTime); + return page(page, wrapper).convert(item -> TransportReconciliationWrapper.build().entityVO(item)); + } + + @Override + public IPage formalOptions(IPage page, String settlementType, String keyword) { + List usedIds = list(Wrappers.lambdaQuery() + .select(TransportReconciliation::getFormalSettlementId)) + .stream().map(TransportReconciliation::getFormalSettlementId).filter(Objects::nonNull).toList(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(FormalSettlement::getApprovalStatus, "approved") + .eq(Func.isNotEmpty(settlementType), FormalSettlement::getSettlementType, settlementType) + .and(Func.isNotEmpty(keyword), value -> value.like(FormalSettlement::getFormalSettlementNo, keyword) + .or().like(FormalSettlement::getContractNo, keyword).or().like(FormalSettlement::getContractName, keyword)); + if (!usedIds.isEmpty()) wrapper.notIn(FormalSettlement::getId, usedIds); + return formalSettlementMapper.selectPage(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); + } + + @Override + public TransportReconciliationVO detail(Long id) { + TransportReconciliationVO vo = TransportReconciliationWrapper.build().entityVO(existing(id)); + vo.setInternalDetails(internalRows(id)); + vo.setExternalDetails(externalRows(id)); + vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() + .eq(TransportReconciliationChangeRecord::getReconciliationId, id) + .orderByDesc(TransportReconciliationChangeRecord::getChangeTime))); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(TransportReconciliationSaveRequest request) { + if (!VEHICLE.equals(request.getReconciliationMode()) && !CARGO.equals(request.getReconciliationMode())) { + throw new ServiceException("请选择正确的对账模式"); + } + FormalSettlement formal = formalSettlementMapper.selectById(request.getFormalSettlementId()); + if (formal == null || !"approved".equals(formal.getApprovalStatus())) throw new ServiceException("请选择已生效的正式结算单"); + long occupied = count(Wrappers.lambdaQuery() + .eq(TransportReconciliation::getFormalSettlementId, formal.getId()) + .ne(request.getId() != null, TransportReconciliation::getId, request.getId())); + if (occupied > 0) throw new ServiceException("该正式结算单已归属其他对账单"); + TransportReconciliation bill = request.getId() == null ? new TransportReconciliation() : editable(request.getId()); + boolean rebuild = bill.getId() == null || !Objects.equals(bill.getFormalSettlementId(), formal.getId()) + || !Objects.equals(bill.getReconciliationMode(), request.getReconciliationMode()); + if (bill.getId() == null) { + bill.setReconciliationNo(nextNo()); + bill.setReconciliationStatus(UNFINISHED); + bill.setMatchStatus(UNMATCHED); + bill.setBillUpdated(false); + } + copyHeader(formal, bill); + bill.setReconciliationMode(request.getReconciliationMode()); + bill.setReconcilerId(AuthUtil.getUserId()); + bill.setReconcilerName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); + bill.setReconciliationDate(request.getReconciliationDate() == null ? LocalDate.now() : request.getReconciliationDate()); + bill.setRemark(limit(request.getRemark(), 200)); + saveOrUpdate(bill); + if (rebuild) { + clearDetails(bill.getId()); + buildInternalRows(bill, formal); + } + refreshStats(bill.getId()); + return bill.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + editable(id); + clearDetails(id); + changeRecordMapper.delete(Wrappers.lambdaQuery() + .eq(TransportReconciliationChangeRecord::getReconciliationId, id)); + removeById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List importVehicles(Long id, List rows) { + TransportReconciliation bill = editable(id); + if (!VEHICLE.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是整车总额对账"); + resetExternal(id); + List failures = new ArrayList<>(); + for (int index = 0; index < rows.size(); index++) { + VehicleReconciliationExcel row = rows.get(index); + try { + validateExternal(row.getVehicleNo(), row.getCargoName(), row.getTransportQuantity(), row.getSettlementAmount()); + TransportReconciliationExternal external = new TransportReconciliationExternal(); + BeanUtil.copyProperties(row, external); + external.setReconciliationId(id); external.setExternalLineNo(index + 2); + external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); + external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目1", money(row.getFeeItemOne())))); + external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); + external.setRawDataJson(JsonUtil.toJson(row)); externalMapper.insert(external); + } catch (Exception exception) { + VehicleReconciliationFailureExcel failure = new VehicleReconciliationFailureExcel(); + BeanUtil.copyProperties(row, failure); failure.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + failures.add(failure); + } + } + refreshStats(id); + return failures; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List importCargoes(Long id, List rows) { + TransportReconciliation bill = editable(id); + if (!CARGO.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是货物明细对账"); + resetExternal(id); + List failures = new ArrayList<>(); + for (int index = 0; index < rows.size(); index++) { + CargoReconciliationExcel row = rows.get(index); + try { + validateExternal(row.getVehicleNo(), row.getCargoName(), row.getTransportQuantity(), row.getSettlementAmount()); + TransportReconciliationExternal external = new TransportReconciliationExternal(); + BeanUtil.copyProperties(row, external); + external.setReconciliationId(id); external.setExternalLineNo(index + 2); + external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); + external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + external.setFeeItemsJson(JsonUtil.toJson(Map.of("费用项目名称1", money(row.getFeeItemOne()), "费用项目名称2", money(row.getFeeItemTwo())))); + external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); + external.setRawDataJson(JsonUtil.toJson(row)); externalMapper.insert(external); + } catch (Exception exception) { + CargoReconciliationFailureExcel failure = new CargoReconciliationFailureExcel(); + BeanUtil.copyProperties(row, failure); failure.setErrorMessage("第" + (index + 2) + "行:" + exception.getMessage()); + failures.add(failure); + } + } + refreshStats(id); + return failures; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void autoMatch(Long id) { + TransportReconciliation bill = editable(id); + List internals = internalRows(id); + List externals = externalRows(id); + if (externals.isEmpty()) throw new ServiceException("请先导入外部账单"); + resetMatches(internals, externals); + Function externalKey = CARGO.equals(bill.getReconciliationMode()) + ? this::cargoKey : this::vehicleKey; + Map> externalGroups = externals.stream().collect(Collectors.groupingBy(externalKey)); + Map> internalGroups = internals.stream().collect(Collectors.groupingBy( + CARGO.equals(bill.getReconciliationMode()) ? this::cargoKey : this::vehicleKey)); + for (Map.Entry> entry : externalGroups.entrySet()) { + List externalGroup = entry.getValue(); + List internalGroup = internalGroups.getOrDefault(entry.getKey(), List.of()); + if (externalGroup.size() == 1 && internalGroup.size() == 1 + && equalMoney(externalGroup.get(0).getSettlementAmount(), internalGroup.get(0).getSettlementAmount())) { + link(internalGroup.get(0), externalGroup.get(0)); + } else if (externalGroup.size() > 1) { + for (TransportReconciliationExternal external : externalGroup) { + external.setSuspectedDuplicate(true); external.setMatchStatus(DUPLICATE); externalMapper.updateById(external); + } + } + } + refreshStats(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void manualMatch(TransportReconciliationManualMatchRequest request) { + editable(request.getReconciliationId()); + TransportReconciliationInternal internal = internalMapper.selectById(request.getInternalId()); + TransportReconciliationExternal external = externalMapper.selectById(request.getExternalId()); + if (internal == null || external == null || !Objects.equals(internal.getReconciliationId(), request.getReconciliationId()) + || !Objects.equals(external.getReconciliationId(), request.getReconciliationId())) throw new ServiceException("匹配明细不存在"); + unlinkInternal(internal); + if (external.getMatchedInternalId() != null) { + TransportReconciliationInternal old = internalMapper.selectById(external.getMatchedInternalId()); + if (old != null) unlinkInternal(old); + } + link(internal, external); + refreshStats(request.getReconciliationId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void unmatch(Long internalId) { + TransportReconciliationInternal internal = internalMapper.selectById(internalId); + if (internal == null) throw new ServiceException("内部账单明细不存在"); + editable(internal.getReconciliationId()); + unlinkInternal(internal); + refreshStats(internal.getReconciliationId()); + } + + @Override + public void adjustInternal(TransportReconciliationInternal row) { + TransportReconciliationInternal internal = internalMapper.selectById(row.getId()); + if (internal == null) throw new ServiceException("内部账单明细不存在"); + editable(internal.getReconciliationId()); + internal.setTransportQuantity(nonNegative(row.getTransportQuantity(), "运输量")); + internal.setUnitPrice(nonNegative(row.getUnitPrice(), "运输单价")); + internal.setMileage(nonNegative(row.getMileage(), "里程")); + internal.setFreightAmount(nonNegative(row.getFreightAmount(), "运输费")); + internal.setFeeItemsJson(row.getFeeItemsJson()); + internal.setSettlementAmount(nonNegative(row.getSettlementAmount(), "结算金额")); + internal.setUpdateResult("manually_adjusted"); + internalMapper.updateById(internal); + refreshStats(internal.getReconciliationId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateByMatch(Long id) { + TransportReconciliation bill = editable(id); + List internals = assertAllMatched(bill); + if (money(bill.getExternalAmount()).compareTo(money(bill.getPaidAmount())) < 0) { + throw new ServiceException("导入账单匹配金额小于已付金额,不能更新内部账单"); + } + Map externalMap = externalRows(id).stream() + .collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity())); + for (TransportReconciliationInternal internal : internals) { + TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId()); + if (VEHICLE.equals(bill.getReconciliationMode()) && hasMultipleCargo(internal.getFormalSettlementDetailId())) { + internal.setUpdateResult("skipped_multi_cargo"); + internal.setUpdateMessage("一车多货,已跳过自动更新,请人工调整货物费用"); + internalMapper.updateById(internal); + continue; + } + applyAmount(bill, internal, external); + } + recalculateSettlement(bill); + bill.setBillUpdated(true); + updateById(bill); + refreshStats(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void complete(Long id) { + TransportReconciliation bill = editable(id); + assertAllMatched(bill); + refreshStats(id); + bill = existing(id); + if (bill.getDifferenceCount() != 0 || money(bill.getDifferenceQuantity()).compareTo(BigDecimal.ZERO) != 0 + || money(bill.getDifferenceAmount()).compareTo(BigDecimal.ZERO) != 0) { + throw new ServiceException("差异单数、差异货量和差异金额必须全部为0才可完成对账"); + } + bill.setReconciliationStatus(COMPLETED); + bill.setCompletedTime(LocalDateTime.now()); + updateById(bill); + } + + private void buildInternalRows(TransportReconciliation bill, FormalSettlement formal) { + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()).orderByAsc(FormalSettlementDetail::getLineNo)); + int lineNo = 1; + for (FormalSettlementDetail detail : details) { + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()).orderByAsc(FormalSettlementDetailFee::getLineNo)); + if (CARGO.equals(bill.getReconciliationMode()) && !fees.isEmpty()) { + for (FormalSettlementDetailFee fee : fees) insertInternal(bill.getId(), detail, fee, lineNo++); + } else { + insertInternal(bill.getId(), detail, null, lineNo++); + } + } + } + + private void insertInternal(Long billId, FormalSettlementDetail detail, FormalSettlementDetailFee fee, int lineNo) { + TransportReconciliationInternal row = new TransportReconciliationInternal(); + BeanUtil.copyProperties(detail, row); + row.setId(null); row.setReconciliationId(billId); row.setFormalSettlementDetailId(detail.getId()); + row.setSourceDetailId(detail.getSourceDetailId()); row.setLineNo(lineNo); row.setMatchResult(UNMATCHED); row.setUpdateResult("not_updated"); + if (fee != null) { + row.setFormalSettlementDetailFeeId(fee.getId()); row.setSourceCargoFeeId(fee.getSourceFeeId()); + row.setCargoName(fee.getCargoName()); row.setCargoType(fee.getCargoType()); row.setTransportQuantity(fee.getTransportQuantity()); + row.setQuantityUnit(fee.getQuantityUnit()); row.setMileage(fee.getMileage()); row.setUnitPrice(fee.getUnitPrice()); + row.setFreightAmount(fee.getFreightAmount()); row.setFeeItemsJson(fee.getFeeItemsJson()); row.setSettlementAmount(fee.getSettlementAmountTax()); + ReceivablePayableCargoFee sourceFee = fee.getSourceFeeId() == null ? null : cargoFeeMapper.selectById(fee.getSourceFeeId()); + if (sourceFee != null) { row.setSpecification(sourceFee.getSpecification()); row.setModel(sourceFee.getModel()); } + } else { + row.setSettlementAmount(detail.getSettlementAmountTax()); + } + internalMapper.insert(row); + } + + private void applyAmount(TransportReconciliation bill, TransportReconciliationInternal internal, TransportReconciliationExternal external) { + BigDecimal before = money(internal.getSettlementAmount()); + BigDecimal after = money(external.getSettlementAmount()); + if (internal.getFormalSettlementDetailFeeId() != null) { + FormalSettlementDetailFee fee = formalDetailFeeMapper.selectById(internal.getFormalSettlementDetailFeeId()); + fee.setSettlementAmountTax(after); fee.setAdjustAmount(after.subtract(money(fee.getOriginalAmount()))); formalDetailFeeMapper.updateById(fee); + if (internal.getSourceCargoFeeId() != null) { + ReceivablePayableCargoFee sourceFee = cargoFeeMapper.selectById(internal.getSourceCargoFeeId()); + if (sourceFee != null) { sourceFee.setAfterAmount(after); sourceFee.setAdjustAmount(after.subtract(money(sourceFee.getOriginalAmount()))); cargoFeeMapper.updateById(sourceFee); } + } + } else { + FormalSettlementDetail detail = formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); + detail.setSettlementAmountTax(after); detail.setAdjustAmount(after.subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + ReceivablePayableDetail source = receivablePayableMapper.selectById(internal.getSourceDetailId()); + if (source != null) { source.setTotalAmount(after); receivablePayableMapper.updateById(source); } + } + internal.setSettlementAmount(after); internal.setUpdateResult("updated"); internal.setUpdateMessage("已按外部账单更新"); internalMapper.updateById(internal); + TransportReconciliationChangeRecord record = new TransportReconciliationChangeRecord(); + record.setReconciliationId(bill.getId()); record.setInternalDetailId(internal.getId()); record.setFormalSettlementId(bill.getFormalSettlementId()); + record.setFormalSettlementDetailId(internal.getFormalSettlementDetailId()); record.setSourceDetailId(internal.getSourceDetailId()); + record.setDocumentNo(internal.getDocumentNo()); record.setCargoName(internal.getCargoName()); record.setBeforeAmount(before); record.setAfterAmount(after); + record.setBeforeDataJson(JsonUtil.toJson(Map.of("settlementAmount", before))); record.setAfterDataJson(JsonUtil.toJson(external)); + record.setOperatorId(AuthUtil.getUserId()); record.setOperatorName(AuthUtil.getUserName()); record.setChangeTime(LocalDateTime.now()); + record.setChangeReason("运输对账按匹配结果更新"); changeRecordMapper.insert(record); + } + + private void recalculateSettlement(TransportReconciliation bill) { + List details = formalDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, bill.getFormalSettlementId())); + for (FormalSettlementDetail detail : details) { + List fees = formalDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())); + if (!fees.isEmpty()) { + BigDecimal total = fees.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + detail.setSettlementAmountTax(total); detail.setAdjustAmount(total.subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail); + ReceivablePayableDetail source = receivablePayableMapper.selectById(detail.getSourceDetailId()); + if (source != null) { source.setTotalAmount(total); receivablePayableMapper.updateById(source); } + } + } + BigDecimal total = details.stream().map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + FormalSettlement formal = formalSettlementMapper.selectById(bill.getFormalSettlementId()); + formal.setSettlementAmount(total); formal.setLocalSettlementAmount(total.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate())); + formalSettlementMapper.updateById(formal); + bill.setSettlementAmount(total); + } + + 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()))) { + throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复"); + } + return internals; + } + + private void refreshStats(Long id) { + TransportReconciliation bill = existing(id); + List internals = internalRows(id); + List externals = externalRows(id); + int matched = (int) internals.stream().filter(item -> MATCHED.equals(item.getMatchResult())).count(); + bill.setInternalBillCount(internals.size()); bill.setExternalBillCount(externals.size()); + int internalUnmatched = (int) internals.stream().filter(item -> !MATCHED.equals(item.getMatchResult())).count(); + int externalUnmatched = (int) externals.stream().filter(item -> !MATCHED.equals(item.getMatchStatus())).count(); + bill.setMatchedCount(matched); bill.setUnmatchedCount(internalUnmatched + externalUnmatched); + bill.setDifferenceCount(Math.abs(internals.size() - externals.size()) + Math.min(internalUnmatched, externalUnmatched)); + bill.setInternalQuantity(sumInternalQuantity(internals)); bill.setExternalQuantity(sumExternalQuantity(externals)); + bill.setDifferenceQuantity(bill.getInternalQuantity().subtract(bill.getExternalQuantity()).abs()); + bill.setInternalAmount(sumInternalAmount(internals)); bill.setExternalAmount(sumExternalAmount(externals)); + bill.setDifferenceAmount(bill.getInternalAmount().subtract(bill.getExternalAmount()).abs()); + bill.setMatchStatus(internals.size() > 0 && internals.size() == externals.size() && matched == internals.size() ? MATCHED : matched > 0 ? "partial" : UNMATCHED); + updateById(bill); + } + + private void link(TransportReconciliationInternal internal, TransportReconciliationExternal external) { + internal.setMatchedExternalId(external.getId()); internal.setMatchedExternalLineNo(external.getExternalLineNo()); internal.setMatchResult(MATCHED); internalMapper.updateById(internal); + external.setMatchedInternalId(internal.getId()); external.setMatchStatus(MATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); + } + + private void unlinkInternal(TransportReconciliationInternal internal) { + if (internal.getMatchedExternalId() != null) { + TransportReconciliationExternal external = externalMapper.selectById(internal.getMatchedExternalId()); + if (external != null) { external.setMatchedInternalId(null); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); } + } + internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); + } + + private void resetMatches(List internals, List externals) { + for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); } + for (TransportReconciliationExternal external : externals) { external.setMatchedInternalId(null); external.setMatchStatus(UNMATCHED); external.setSuspectedDuplicate(false); externalMapper.updateById(external); } + } + + private void resetExternal(Long id) { + List internals = internalRows(id); + for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); } + externalMapper.delete(Wrappers.lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id)); + } + + private void clearDetails(Long id) { + internalMapper.delete(Wrappers.lambdaQuery().eq(TransportReconciliationInternal::getReconciliationId, id)); + externalMapper.delete(Wrappers.lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id)); + } + + private void copyHeader(FormalSettlement formal, TransportReconciliation bill) { + bill.setFormalSettlementId(formal.getId()); bill.setFormalSettlementNo(formal.getFormalSettlementNo()); bill.setSettlementType(formal.getSettlementType()); + bill.setProjectId(formal.getProjectId()); bill.setProjectName(formal.getProjectName()); bill.setDeptId(formal.getDeptId()); bill.setDeptName(formal.getDeptName()); + bill.setContractId(formal.getContractId()); bill.setContractNo(formal.getContractNo()); bill.setContractName(formal.getContractName()); + bill.setPayerName(formal.getPayerName()); bill.setPayeeName(formal.getPayeeName()); bill.setCurrency(formal.getCurrency()); + bill.setSettlementAmount(money(formal.getSettlementAmount())); bill.setPaidAmount(money(formal.getPaidAmount())); + List preNos = formalSourceMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, formal.getId())) + .stream().map(FormalSettlementSource::getPreSettlementNo).filter(Func::isNotEmpty).toList(); + bill.setPreSettlementNos(String.join(",", preNos)); + } + + private TransportReconciliation existing(Long id) { + TransportReconciliation bill = getById(id); + if (bill == null) throw new ServiceException("运输对账单不存在"); + return bill; + } + + private TransportReconciliation editable(Long id) { + TransportReconciliation bill = existing(id); + if (!UNFINISHED.equals(bill.getReconciliationStatus())) throw new ServiceException("已完成的运输对账单禁止修改或删除"); + return bill; + } + + private List internalRows(Long id) { + return internalMapper.selectList(Wrappers.lambdaQuery().eq(TransportReconciliationInternal::getReconciliationId, id).orderByAsc(TransportReconciliationInternal::getLineNo)); + } + + private List externalRows(Long id) { + return externalMapper.selectList(Wrappers.lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id).orderByAsc(TransportReconciliationExternal::getExternalLineNo)); + } + + private boolean hasMultipleCargo(Long formalDetailId) { + return formalDetailFeeMapper.selectCount(Wrappers.lambdaQuery().eq(FormalSettlementDetailFee::getFormalSettlementDetailId, formalDetailId)) > 1; + } + + private void validateExternal(String vehicleNo, String cargoName, BigDecimal quantity, BigDecimal amount) { + if (Func.isEmpty(vehicleNo)) throw new ServiceException("车牌号不能为空"); + if (Func.isEmpty(cargoName)) throw new ServiceException("货物名称不能为空"); + nonNegative(quantity, "运输量"); nonNegative(amount, "结算金额"); + } + + private LocalDateTime parseTime(String value, String field) { + if (Func.isEmpty(value)) throw new ServiceException(field + "不能为空"); + LocalDateTime result = parseTimeNullable(value, field); + if (result == null) throw new ServiceException(field + "格式错误"); + return result; + } + + private LocalDateTime parseTimeNullable(String value, String field) { + if (Func.isEmpty(value)) return null; + for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm")) { + try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { } + } + throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss"); + } + + private String vehicleKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), row.getCargoName(), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); } + private String vehicleKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getCargoName(), row.getActualDepartureTime(), row.getBatchNo(), row.getTransportQuantity()); } + private String cargoKey(TransportReconciliationInternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); } + private String cargoKey(TransportReconciliationExternal row) { return key(row.getVehicleNo(), row.getDepartureAddress(), row.getArrivalAddress(), row.getCargoName(), row.getActualDepartureTime(), row.getTransportQuantity()); } + private String key(Object... values) { StringBuilder builder = new StringBuilder(); for (Object value : values) builder.append(normal(value)).append('|'); return builder.toString(); } + private String normal(Object value) { if (value == null) return ""; if (value instanceof BigDecimal decimal) return decimal.stripTrailingZeros().toPlainString(); return value.toString().trim().replaceAll("\\s+", "").toLowerCase(); } + private boolean equalMoney(BigDecimal left, BigDecimal right) { return money(left).compareTo(money(right)) == 0; } + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; } + private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; } + private BigDecimal sumInternalQuantity(List rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal sumExternalQuantity(List rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal sumInternalAmount(List rows) { return rows.stream().map(TransportReconciliationInternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal sumExternalAmount(List rows) { return rows.stream().map(TransportReconciliationExternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private String nextNo() { return "DZ" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java new file mode 100644 index 0000000..c794d95 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/TransportReconciliationWrapper.java @@ -0,0 +1,30 @@ +/** BladeX Commercial License Agreement. Copyright (c) 2018-2099, https://bladex.cn. Author: Chill Zhuang. */ +package org.springblade.transport.wrapper; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.system.cache.UserCache; +import org.springblade.transport.pojo.entity.TransportReconciliation; +import org.springblade.transport.pojo.vo.TransportReconciliationVO; + +import java.util.Objects; + +/** 运输对账单包装类。 @author Chill */ +public class TransportReconciliationWrapper extends BaseEntityWrapper { + public static TransportReconciliationWrapper build() { return new TransportReconciliationWrapper(); } + + @Override + public TransportReconciliationVO entityVO(TransportReconciliation entity) { + TransportReconciliationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, TransportReconciliationVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setReconciliationModeName("cargo".equals(entity.getReconciliationMode()) ? "货物明细" : "整车总额"); + vo.setReconciliationStatusName("completed".equals(entity.getReconciliationStatus()) ? "已完成" : "未完成"); + vo.setMatchStatusName(switch (entity.getMatchStatus() == null ? "" : entity.getMatchStatus()) { + case "matched" -> "已匹配"; + case "partial" -> "部分匹配"; + default -> "未匹配"; + }); + return vo; + } +} diff --git a/doc/sql/transport/blade_settlement_adjustment_20260818.sql b/doc/sql/transport/blade_settlement_adjustment_20260818.sql new file mode 100644 index 0000000..5c63d71 --- /dev/null +++ b/doc/sql/transport/blade_settlement_adjustment_20260818.sql @@ -0,0 +1,39 @@ +-- 结算管理 / 结算调整单 +CREATE TABLE IF NOT EXISTS `blade_settlement_adjustment` ( + `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, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `adjustment_no` varchar(100) NOT NULL COMMENT '结算调整单号', `formal_settlement_id` bigint(20) NOT NULL COMMENT '关联正式结算单ID', + `formal_settlement_no` varchar(100) NOT NULL COMMENT '关联正式结算单号', `settlement_type` varchar(30) NOT NULL, + `project_name` varchar(100) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, `customer_name` varchar(200) DEFAULT NULL, + `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) DEFAULT NULL, + `adjustment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `original_settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原结算金额', + `adjusted_settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整后结算金额', + `approval_status` varchar(30) NOT NULL DEFAULT 'draft', `current_node` varchar(100) DEFAULT NULL, + `current_processor` varchar(200) DEFAULT NULL, `kingdee_sync_status` varchar(30) DEFAULT NULL COMMENT '关联正式单金蝶状态', `remark` varchar(200) DEFAULT NULL, `approved_time` datetime DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_settlement_adjustment_no` (`tenant_id`,`adjustment_no`), + KEY `idx_adjustment_formal` (`formal_settlement_id`), KEY `idx_adjustment_status` (`approval_status`), KEY `idx_adjustment_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='结算调整单'; + +CREATE TABLE IF NOT EXISTS `blade_settlement_adjustment_detail` ( + `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, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `adjustment_id` bigint(20) NOT NULL, `formal_settlement_detail_id` bigint(20) NOT NULL, `formal_settlement_detail_fee_id` bigint(20) NOT NULL, + `fee_type` varchar(100) DEFAULT NULL, `fee_item` varchar(200) DEFAULT NULL, `original_amount_tax` decimal(18,2) DEFAULT NULL, + `adjustment_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', `adjustment_amount_no_tax` decimal(18,2) DEFAULT NULL, + `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_adjustment_detail_bill` (`adjustment_id`), + KEY `idx_adjustment_detail_fee` (`formal_settlement_detail_fee_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='结算调整费用明细'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000001060,2090000000000001000,'settlement_adjustment','结算调整单','settlement_adjustment','/settlement/settlement-adjustment','iconfont icon-caidanguanli',5,1,0,1,NULL,'',0), +(2090000000000001061,2090000000000001060,'settlement_adjustment_view','查看','settlement_adjustment_view','','',1,2,0,1,NULL,'',0), +(2090000000000001062,2090000000000001060,'settlement_adjustment_add','新增','settlement_adjustment_add','','',2,2,0,1,NULL,'',0), +(2090000000000001063,2090000000000001060,'settlement_adjustment_edit','编辑','settlement_adjustment_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001064,2090000000000001060,'settlement_adjustment_delete','删除','settlement_adjustment_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001065,2090000000000001060,'settlement_adjustment_submit','提交审批','settlement_adjustment_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001066,2090000000000001060,'settlement_adjustment_approve','审批','settlement_adjustment_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001067,2090000000000001060,'settlement_adjustment_repush','重新推送金蝶','settlement_adjustment_repush','','',7,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_transport_reconciliation_20260818.sql b/doc/sql/transport/blade_transport_reconciliation_20260818.sql new file mode 100644 index 0000000..9c539e1 --- /dev/null +++ b/doc/sql/transport/blade_transport_reconciliation_20260818.sql @@ -0,0 +1,82 @@ +-- 结算管理 / 运输对账 +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation` ( + `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, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_no` varchar(100) NOT NULL, `formal_settlement_id` bigint(20) NOT NULL, `formal_settlement_no` varchar(100) NOT NULL, + `pre_settlement_nos` varchar(1000) DEFAULT NULL, `settlement_type` varchar(30) NOT NULL, `reconciliation_mode` varchar(30) NOT NULL, + `project_id` bigint(20) DEFAULT NULL, `project_name` varchar(100) DEFAULT NULL, `dept_id` bigint(20) DEFAULT NULL, `dept_name` varchar(100) DEFAULT NULL, + `contract_id` bigint(20) DEFAULT NULL, `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(200) DEFAULT NULL, + `payer_name` varchar(200) DEFAULT NULL, `payee_name` varchar(200) DEFAULT NULL, `currency` varchar(20) DEFAULT 'RMB', + `settlement_amount` decimal(18,2) DEFAULT 0, `paid_amount` decimal(18,2) DEFAULT 0, `reconciler_id` bigint(20) DEFAULT NULL, + `reconciler_name` varchar(100) DEFAULT NULL, `reconciliation_date` date DEFAULT NULL, `reconciliation_status` varchar(30) NOT NULL DEFAULT 'unfinished', + `match_status` varchar(30) NOT NULL DEFAULT 'unmatched', `internal_bill_count` int(11) DEFAULT 0, `external_bill_count` int(11) DEFAULT 0, + `difference_count` int(11) DEFAULT 0, `internal_quantity` decimal(18,6) DEFAULT 0, `external_quantity` decimal(18,6) DEFAULT 0, + `difference_quantity` decimal(18,6) DEFAULT 0, `internal_amount` decimal(18,2) DEFAULT 0, `external_amount` decimal(18,2) DEFAULT 0, + `difference_amount` decimal(18,2) DEFAULT 0, `matched_count` int(11) DEFAULT 0, `unmatched_count` int(11) DEFAULT 0, + `bill_updated` tinyint(1) DEFAULT 0, `completed_time` datetime DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_transport_reconciliation_no` (`tenant_id`,`reconciliation_no`), + KEY `idx_transport_reconciliation_formal` (`formal_settlement_id`), KEY `idx_transport_reconciliation_type` (`settlement_type`), + KEY `idx_transport_reconciliation_status` (`reconciliation_status`,`match_status`), KEY `idx_transport_reconciliation_create` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账单'; + +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation_internal` ( + `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, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_id` bigint(20) NOT NULL, `formal_settlement_detail_id` bigint(20) NOT NULL, `formal_settlement_detail_fee_id` bigint(20) DEFAULT NULL, + `source_detail_id` bigint(20) DEFAULT NULL, `source_cargo_fee_id` bigint(20) DEFAULT NULL, `line_no` int(11) NOT NULL, + `document_no` varchar(100) DEFAULT NULL, `waybill_no` varchar(100) DEFAULT NULL, `vehicle_no` varchar(100) DEFAULT NULL, + `departure_address` varchar(500) DEFAULT NULL, `arrival_address` varchar(500) DEFAULT NULL, `actual_departure_time` datetime DEFAULT NULL, + `actual_completion_time` datetime DEFAULT NULL, `transport_type` varchar(100) DEFAULT NULL, `cargo_name` varchar(500) DEFAULT NULL, + `cargo_type` varchar(500) DEFAULT NULL, `specification` varchar(200) DEFAULT NULL, `model` varchar(200) DEFAULT NULL, + `transport_quantity` decimal(18,6) DEFAULT NULL, `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, + `batch_no` varchar(100) DEFAULT NULL, `unit_price` decimal(18,2) DEFAULT NULL, `freight_amount` decimal(18,2) DEFAULT 0, + `fee_items_json` longtext, `settlement_amount` decimal(18,2) DEFAULT 0, `matched_external_id` bigint(20) DEFAULT NULL, + `matched_external_line_no` int(11) DEFAULT NULL, `match_result` varchar(30) DEFAULT 'unmatched', `update_result` varchar(30) DEFAULT 'not_updated', + `update_message` varchar(500) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_recon_internal_bill` (`reconciliation_id`), + KEY `idx_recon_internal_source` (`source_detail_id`), KEY `idx_recon_internal_match` (`matched_external_id`), + KEY `idx_recon_internal_vehicle` (`vehicle_no`,`cargo_name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账内部账单快照'; + +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation_external` ( + `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, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_id` bigint(20) NOT NULL, `external_line_no` int(11) NOT NULL, `vehicle_no` varchar(100) DEFAULT NULL, + `departure_address` varchar(500) DEFAULT NULL, `arrival_address` varchar(500) DEFAULT NULL, `actual_departure_time` datetime DEFAULT NULL, + `actual_completion_time` datetime DEFAULT NULL, `transport_type` varchar(100) DEFAULT NULL, `cargo_name` varchar(500) DEFAULT NULL, + `cargo_type` varchar(500) DEFAULT NULL, `specification` varchar(200) DEFAULT NULL, `model` varchar(200) DEFAULT NULL, + `transport_quantity` decimal(18,6) DEFAULT NULL, `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, + `batch_no` varchar(100) DEFAULT NULL, `unit_price` decimal(18,2) DEFAULT NULL, `freight_amount` decimal(18,2) DEFAULT 0, + `fee_items_json` longtext, `settlement_amount` decimal(18,2) DEFAULT 0, `suspected_duplicate` tinyint(1) DEFAULT 0, + `match_status` varchar(30) DEFAULT 'unmatched', `matched_internal_id` bigint(20) DEFAULT NULL, `error_message` varchar(500) DEFAULT NULL, + `raw_data_json` longtext, PRIMARY KEY (`id`), KEY `idx_recon_external_bill` (`reconciliation_id`), KEY `idx_recon_external_line` (`reconciliation_id`,`external_line_no`), + KEY `idx_recon_external_match` (`matched_internal_id`), KEY `idx_recon_external_status` (`match_status`,`suspected_duplicate`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账外部账单'; + +CREATE TABLE IF NOT EXISTS `blade_transport_reconciliation_change_record` ( + `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, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT 1, `is_deleted` int(11) DEFAULT 0, + `reconciliation_id` bigint(20) NOT NULL, `internal_detail_id` bigint(20) DEFAULT NULL, `formal_settlement_id` bigint(20) DEFAULT NULL, + `formal_settlement_detail_id` bigint(20) DEFAULT NULL, `source_detail_id` bigint(20) DEFAULT NULL, `document_no` varchar(100) DEFAULT NULL, + `cargo_name` varchar(500) DEFAULT NULL, `before_amount` decimal(18,2) DEFAULT 0, `after_amount` decimal(18,2) DEFAULT 0, + `before_data_json` longtext, `after_data_json` longtext, `operator_id` bigint(20) DEFAULT NULL, `operator_name` varchar(100) DEFAULT NULL, + `change_time` datetime DEFAULT NULL, `change_reason` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), + KEY `idx_recon_change_bill` (`reconciliation_id`), KEY `idx_recon_change_source` (`source_detail_id`), KEY `idx_recon_change_time` (`change_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='运输对账账单变更记录'; + +INSERT INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES +(2090000000000002060,2090000000000001000,'transport_reconciliation','运输对账','transport_reconciliation','/settlement/transport-reconciliation','iconfont icon-caidanguanli',5,1,0,1,NULL,'',0), +(2090000000000002061,2090000000000002060,'transport_reconciliation_view','查看','transport_reconciliation_view','', '',1,2,0,1,NULL,'',0), +(2090000000000002062,2090000000000002060,'transport_reconciliation_add','新增','transport_reconciliation_add','', '',2,2,0,1,NULL,'',0), +(2090000000000002063,2090000000000002060,'transport_reconciliation_edit','编辑','transport_reconciliation_edit','', '',3,2,0,1,NULL,'',0), +(2090000000000002064,2090000000000002060,'transport_reconciliation_delete','删除','transport_reconciliation_delete','', '',4,2,0,1,NULL,'',0), +(2090000000000002065,2090000000000002060,'transport_reconciliation_import','导入外部账单','transport_reconciliation_import','', '',5,2,0,1,NULL,'',0), +(2090000000000002066,2090000000000002060,'transport_reconciliation_match','开始匹配','transport_reconciliation_match','', '',6,2,0,1,NULL,'',0), +(2090000000000002067,2090000000000002060,'transport_reconciliation_update','按匹配结果更新','transport_reconciliation_update','', '',7,2,0,1,NULL,'',0), +(2090000000000002068,2090000000000002060,'transport_reconciliation_complete','完成对账','transport_reconciliation_complete','', '',8,2,0,1,NULL,'',0), +(2090000000000002069,2090000000000002060,'transport_reconciliation_export','导出','transport_reconciliation_export','', '',9,2,0,1,NULL,'',0), +(2090000000000002070,2090000000000002060,'transport_reconciliation_adjust','明细调整','transport_reconciliation_adjust','', '',10,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0;