1、新增结算调整单

2、新增运输对账
This commit is contained in:
2026-08-19 06:53:39 +08:00
parent 8bb18c6f6a
commit df6d417e40
37 changed files with 1843 additions and 43 deletions
@@ -75,6 +75,10 @@ public class ApiScopePermissionHandler implements IPermissionHandler {
if (request == null || user == null) {
return false;
}
// 超级管理员在菜单授权树中默认拥有全部菜单权限,与框架默认处理器保持一致。
if (AuthUtil.isAdministrator()) {
return true;
}
List<String> codes = permissionMenu(permission, user.getRoleId());
return codes != null && !codes.isEmpty();
}
@@ -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<Long> sourceDetailIds;
@Schema(description = "是否允许批量转结算兼容历史来源明细的项目、所属组织或客商差异")
private Boolean allowSourceMismatch;
@Schema(description = "结算合计费用")
private List<SummaryFee> summaryFees;
@@ -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<Detail> 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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -0,0 +1,68 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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;
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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;
}
@@ -0,0 +1,49 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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;
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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;
}
@@ -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<SettlementAdjustmentDetail> details;
private List<FormalSettlementDetail> formalDetails;
}
@@ -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<TransportReconciliationInternal> internalDetails;
@TableField(exist = false) private List<TransportReconciliationExternal> externalDetails;
@TableField(exist = false) private List<TransportReconciliationChangeRecord> changeRecords;
}
@@ -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<SettlementAdjustmentVO> selectPage(IPage<SettlementAdjustment> page, SettlementAdjustmentVO query);
SettlementAdjustmentVO detail(Long id);
List<Map<String, Object>> candidateFormalSettlements(String keyword);
List<Map<String, Object>> 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);
}
@@ -120,9 +120,10 @@ public class ReceivablePayableDetailController extends BladeController {
@RequestParam(required = false) String batchNo,
@RequestParam(required = false) String generateStartDate,
@RequestParam(required = false) String generateEndDate,
@RequestParam(required = false) String settlementBillType) {
@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")
@@ -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<IPage<SettlementAdjustmentVO>> list(SettlementAdjustmentVO query, Query page) { return R.data(service.selectPage(Condition.getPage(page), query)); }
@GetMapping("/detail") public R<SettlementAdjustmentVO> detail(@RequestParam Long id) { return R.data(service.detail(id)); }
@GetMapping("/candidate-formal-settlements") public R<List<Map<String, Object>>> candidates(@RequestParam(required = false) String keyword) { return R.data(service.candidateFormalSettlements(keyword)); }
@GetMapping("/formal-details") public R<List<Map<String, Object>>> formalDetails(@RequestParam Long formalSettlementId) { return R.data(service.formalDetails(formalSettlementId)); }
@PostMapping("/save") public R<Long> 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<String> repush(@RequestParam Long id) { return R.data(service.repush(id)); }
}
@@ -0,0 +1,144 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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<IPage<TransportReconciliationVO>> list(TransportReconciliationVO query, Query pageQuery) {
return R.data(reconciliationService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "运输对账详情")
public R<TransportReconciliationVO> detail(@RequestParam Long id) { return R.data(reconciliationService.detail(id)); }
@GetMapping("/formal-options")
@ApiOperationSupport(order = 3)
@Operation(summary = "可选正式结算单")
public R<IPage<FormalSettlement>> 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<Long> 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<VehicleReconciliationFailureExcel> 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<CargoReconciliationFailureExcel> 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>(), CargoReconciliationExcel.class);
else ExcelUtil.export(response, "整车总额对账模板", "整车总额对账模板", new ArrayList<VehicleReconciliationExcel>(), 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("对账单确认完成"); }
}
@@ -0,0 +1,41 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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;
}
@@ -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;
}
@@ -0,0 +1,40 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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;
}
@@ -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;
}
@@ -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<SettlementAdjustmentDetail> {}
@@ -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<SettlementAdjustment> {}
@@ -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<TransportReconciliationChangeRecord> {
}
@@ -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<TransportReconciliationExternal> {
}
@@ -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<TransportReconciliationInternal> {
}
@@ -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<TransportReconciliation> {
}
@@ -58,7 +58,8 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
void transferSettlement(ReceivablePayableTransferRequest request);
IPage<Map<String, Object>> transferCandidates(IPage<?> page, String contractName, String batchNo,
String generateStartDate, String generateEndDate, String settlementBillType);
String generateStartDate, String generateEndDate, String settlementBillType,
String settlementType);
IPage<Map<String, Object>> generateWaybills(IPage<?> page, ReceivablePayableGenerateRequest request);
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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<TransportReconciliation> {
IPage<TransportReconciliationVO> selectPage(IPage<TransportReconciliation> page, TransportReconciliationVO query);
IPage<FormalSettlement> formalOptions(IPage<FormalSettlement> page, String settlementType, String keyword);
TransportReconciliationVO detail(Long id);
Long saveDraft(TransportReconciliationSaveRequest request);
void removeDraft(Long id);
List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows);
List<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> 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);
}
@@ -272,7 +272,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
settlement.setRemark(request.getRemark());
saveOrUpdate(settlement);
if (request.getSourceDetailIds() != null) {
synchronizeDetails(settlement, request.getSourceDetailIds());
synchronizeDetails(settlement, request.getSourceDetailIds(), Boolean.TRUE.equals(request.getAllowSourceMismatch()));
}
settlement.setExchangeRate(normalizeRate(request.getExchangeRate(), settlement.getCurrency()));
rebuildSummaryFees(settlement.getId(), true);
@@ -637,7 +637,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
return waybill == null ? "" : getter.apply(waybill);
}
private void synchronizeDetails(PreSettlement settlement, List<Long> requestedIds) {
private void synchronizeDetails(PreSettlement settlement, List<Long> requestedIds, boolean allowSourceMismatch) {
List<Long> distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList();
List<PreSettlementDetail> existingDetails = listDetails(settlement.getId());
Set<Long> requestedSet = new LinkedHashSet<>(distinctIds);
@@ -661,7 +661,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
boolean hasRetainedDetail = existingDetails.stream()
.anyMatch(detail -> 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<PreSettlementMappe
renumberDetails(settlement.getId());
}
private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source) {
private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source,
boolean allowSourceMismatch) {
if (!Objects.equals(source.getContractId(), settlement.getContractId())) {
throw new ServiceException("仅可选择当前合同的应收应付明细");
}
if (!Objects.equals(source.getSettlementType(), settlement.getSettlementType())) {
throw new ServiceException("应收应付明细的结算类型不一致");
}
if (!Objects.equals(source.getProjectId(), settlement.getProjectId())
if (!allowSourceMismatch && (!Objects.equals(source.getProjectId(), settlement.getProjectId())
|| !Objects.equals(source.getDeptId(), settlement.getDeptId())
|| !List.of(settlement.getPayerName(), settlement.getPayeeName()).contains(source.getCustomerName())) {
|| !List.of(settlement.getPayerName(), settlement.getPayeeName()).contains(source.getCustomerName()))) {
throw new ServiceException("应收应付明细的项目、所属组织或客商与预结算单不一致");
}
if (!"pending".equals(source.getSettlementStatus()) || Func.isNotEmpty(source.getPreSettlementNo())
@@ -712,6 +713,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
private PreSettlementDetail copySourceDetail(PreSettlement settlement, ReceivablePayableDetail source) {
PreSettlementDetail detail = new PreSettlementDetail();
detail.setPreSettlementId(settlement.getId());
// line_no 在数据库中为非空字段,插入后再统一重排前先提供临时行号。
detail.setLineNo(1);
detail.setSourceDetailId(source.getId());
detail.setDocumentNo(source.getDocumentNo());
detail.setWaybillId(source.getWaybillId());
@@ -1092,6 +1095,10 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
}
private String resolveSettlementType(PreSettlementSaveRequest request, ContractManage contract) {
if (Boolean.TRUE.equals(request.getAllowSourceMismatch()) && Func.isNotEmpty(request.getSettlementType())) {
validateSettlementType(request.getSettlementType());
return request.getSettlementType();
}
return contractSettlementType(contract);
}
@@ -37,7 +37,9 @@ import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
import org.springblade.transport.mapper.ReceivablePayableChangeRecordMapper;
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
import org.springblade.transport.pojo.dto.ReceivablePayableAdjustFeeRequest;
import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableGenerateRequest;
import org.springblade.transport.pojo.dto.PreSettlementSaveRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableTransferRequest;
import org.springblade.transport.pojo.dto.ReceivablePayableUpdateFeeRequest;
import org.springblade.transport.pojo.entity.ContractManage;
@@ -52,9 +54,12 @@ import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICommonAddressService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.wrapper.ReceivablePayableDetailWrapper;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
@@ -91,17 +96,23 @@ public class ReceivablePayableDetailServiceImpl
private final IWaybillService waybillService;
private final IContractManageService contractManageService;
private final ICommonAddressService commonAddressService;
private final IPreSettlementService preSettlementService;
private final IFormalSettlementService formalSettlementService;
public ReceivablePayableDetailServiceImpl(ReceivablePayableCargoFeeMapper cargoFeeMapper,
ReceivablePayableChangeRecordMapper changeRecordMapper,
IWaybillService waybillService,
IContractManageService contractManageService,
ICommonAddressService commonAddressService) {
ICommonAddressService commonAddressService,
@Lazy IPreSettlementService preSettlementService,
@Lazy IFormalSettlementService formalSettlementService) {
this.cargoFeeMapper = cargoFeeMapper;
this.changeRecordMapper = changeRecordMapper;
this.waybillService = waybillService;
this.contractManageService = contractManageService;
this.commonAddressService = commonAddressService;
this.preSettlementService = preSettlementService;
this.formalSettlementService = formalSettlementService;
}
@Override
@@ -269,34 +280,60 @@ public class ReceivablePayableDetailServiceImpl
if (details.size() != request.getIds().size()) {
throw new ServiceException("存在无效的费用明细");
}
String billNo = settlementBillNo(request.getSettlementBillType());
for (ReceivablePayableDetail detail : details) {
if (!"pending".equals(detail.getSettlementStatus())) {
throw new ServiceException("仅待结算明细允许转结算");
}
validateTransferDetails(details);
List<Long> detailIds = details.stream().map(ReceivablePayableDetail::getId).toList();
ReceivablePayableDetail first = details.get(0);
if ("pre".equals(request.getSettlementBillType())) {
detail.setPreSettlementNo(billNo);
detail.setSettlementStatus("pre_settled");
} else {
detail.setFormalSettlementNo(billNo);
detail.setSettlementStatus("formal_settled");
PreSettlementSaveRequest saveRequest = new PreSettlementSaveRequest();
saveRequest.setContractId(first.getContractId());
saveRequest.setSettlementType(first.getSettlementType());
saveRequest.setSourceDetailIds(detailIds);
saveRequest.setAllowSourceMismatch(true);
preSettlementService.saveDraft(saveRequest);
return;
}
updateById(detail);
FormalSettlementSaveRequest saveRequest = new FormalSettlementSaveRequest();
saveRequest.setContractId(first.getContractId());
saveRequest.setSettlementType(first.getSettlementType());
saveRequest.setSourceDetailIds(detailIds);
formalSettlementService.saveDraft(saveRequest);
}
private void validateTransferDetails(List<ReceivablePayableDetail> 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<Map<String, Object>> 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<ReceivablePayableDetailVO> detailPage = selectPage(new Page<>(page.getCurrent(), page.getSize()), query);
LambdaQueryWrapper<ReceivablePayableDetail> wrapper = buildQuery(query)
.and(item -> item.isNull(ReceivablePayableDetail::getPreSettlementNo)
.or().eq(ReceivablePayableDetail::getPreSettlementNo, ""))
.and(item -> item.isNull(ReceivablePayableDetail::getFormalSettlementNo)
.or().eq(ReceivablePayableDetail::getFormalSettlementNo, ""));
IPage<ReceivablePayableDetailVO> detailPage = ReceivablePayableDetailWrapper.build()
.pageVO(page(new Page<>(page.getCurrent(), page.getSize()), wrapper));
Page<Map<String, Object>> 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<String, Object> beanMap(ReceivablePayableDetailVO detail) {
private Map<String, Object> candidateMap(ReceivablePayableDetailVO detail) {
Map<String, Object> 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;
}
}
@@ -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<SettlementAdjustmentMapper, SettlementAdjustment>
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<SettlementAdjustmentVO> selectPage(IPage<SettlementAdjustment> page, SettlementAdjustmentVO query) {
LambdaQueryWrapper<SettlementAdjustment> wrapper = Wrappers.<SettlementAdjustment>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.<SettlementAdjustmentDetail>lambdaQuery()
.eq(SettlementAdjustmentDetail::getAdjustmentId, id).orderByAsc(SettlementAdjustmentDetail::getCreateTime)));
vo.setFormalDetails(formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, adjustment.getFormalSettlementId())
.orderByAsc(FormalSettlementDetail::getLineNo)));
return vo;
}
@Override
public List<Map<String, Object>> candidateFormalSettlements(String keyword) {
List<FormalSettlement> rows = formalMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.like(Func.isNotEmpty(keyword), FormalSettlement::getFormalSettlementNo, keyword)
.orderByDesc(FormalSettlement::getCreateTime));
List<Map<String, Object>> result = new ArrayList<>();
for (FormalSettlement row : rows) {
Map<String, Object> 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<Map<String, Object>> formalDetails(Long formalSettlementId) {
FormalSettlement settlement = formalMapper.selectById(formalSettlementId);
if (settlement == null || !APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单可调整");
List<Map<String, Object>> result = new ArrayList<>();
List<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, formalSettlementId).orderByAsc(FormalSettlementDetail::getLineNo));
for (FormalSettlementDetail detail : details) {
for (FormalSettlementDetailFee fee : formalFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId()).orderByAsc(FormalSettlementDetailFee::getLineNo))) {
Map<String, Object> 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.<SettlementAdjustmentDetail>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.<SettlementAdjustmentDetail>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.<SettlementAdjustmentDetail>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.<FormalSettlementDetail>lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()))) {
List<FormalSettlementDetailFee> fees = formalFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>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.<FormalSettlementDetail>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.<SettlementAdjustment>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; }
}
@@ -0,0 +1,561 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.</p>
* <p>Author: Chill Zhuang (bladejava@qq.com)</p>
*/
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<TransportReconciliationMapper, TransportReconciliation>
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<TransportReconciliationVO> selectPage(IPage<TransportReconciliation> page, TransportReconciliationVO query) {
LambdaQueryWrapper<TransportReconciliation> wrapper = Wrappers.<TransportReconciliation>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<FormalSettlement> formalOptions(IPage<FormalSettlement> page, String settlementType, String keyword) {
List<Long> usedIds = list(Wrappers.<TransportReconciliation>lambdaQuery()
.select(TransportReconciliation::getFormalSettlementId))
.stream().map(TransportReconciliation::getFormalSettlementId).filter(Objects::nonNull).toList();
LambdaQueryWrapper<FormalSettlement> wrapper = Wrappers.<FormalSettlement>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.<TransportReconciliationChangeRecord>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.<TransportReconciliation>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.<TransportReconciliationChangeRecord>lambdaQuery()
.eq(TransportReconciliationChangeRecord::getReconciliationId, id));
removeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<VehicleReconciliationFailureExcel> importVehicles(Long id, List<VehicleReconciliationExcel> rows) {
TransportReconciliation bill = editable(id);
if (!VEHICLE.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是整车总额对账");
resetExternal(id);
List<VehicleReconciliationFailureExcel> 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<CargoReconciliationFailureExcel> importCargoes(Long id, List<CargoReconciliationExcel> rows) {
TransportReconciliation bill = editable(id);
if (!CARGO.equals(bill.getReconciliationMode())) throw new ServiceException("当前对账模式不是货物明细对账");
resetExternal(id);
List<CargoReconciliationFailureExcel> 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<TransportReconciliationInternal> internals = internalRows(id);
List<TransportReconciliationExternal> externals = externalRows(id);
if (externals.isEmpty()) throw new ServiceException("请先导入外部账单");
resetMatches(internals, externals);
Function<TransportReconciliationExternal, String> externalKey = CARGO.equals(bill.getReconciliationMode())
? this::cargoKey : this::vehicleKey;
Map<String, List<TransportReconciliationExternal>> externalGroups = externals.stream().collect(Collectors.groupingBy(externalKey));
Map<String, List<TransportReconciliationInternal>> internalGroups = internals.stream().collect(Collectors.groupingBy(
CARGO.equals(bill.getReconciliationMode()) ? this::cargoKey : this::vehicleKey));
for (Map.Entry<String, List<TransportReconciliationExternal>> entry : externalGroups.entrySet()) {
List<TransportReconciliationExternal> externalGroup = entry.getValue();
List<TransportReconciliationInternal> 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<TransportReconciliationInternal> internals = assertAllMatched(bill);
if (money(bill.getExternalAmount()).compareTo(money(bill.getPaidAmount())) < 0) {
throw new ServiceException("导入账单匹配金额小于已付金额,不能更新内部账单");
}
Map<Long, TransportReconciliationExternal> 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<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()).orderByAsc(FormalSettlementDetail::getLineNo));
int lineNo = 1;
for (FormalSettlementDetail detail : details) {
List<FormalSettlementDetailFee> fees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>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<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.eq(FormalSettlementDetail::getFormalSettlementId, bill.getFormalSettlementId()));
for (FormalSettlementDetail detail : details) {
List<FormalSettlementDetailFee> fees = formalDetailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>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<TransportReconciliationInternal> assertAllMatched(TransportReconciliation bill) {
List<TransportReconciliationInternal> internals = internalRows(bill.getId());
List<TransportReconciliationExternal> 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<TransportReconciliationInternal> internals = internalRows(id);
List<TransportReconciliationExternal> 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<TransportReconciliationInternal> internals, List<TransportReconciliationExternal> 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<TransportReconciliationInternal> internals = internalRows(id);
for (TransportReconciliationInternal internal : internals) { internal.setMatchedExternalId(null); internal.setMatchedExternalLineNo(null); internal.setMatchResult(UNMATCHED); internalMapper.updateById(internal); }
externalMapper.delete(Wrappers.<TransportReconciliationExternal>lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id));
}
private void clearDetails(Long id) {
internalMapper.delete(Wrappers.<TransportReconciliationInternal>lambdaQuery().eq(TransportReconciliationInternal::getReconciliationId, id));
externalMapper.delete(Wrappers.<TransportReconciliationExternal>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<String> preNos = formalSourceMapper.selectList(Wrappers.<FormalSettlementSource>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<TransportReconciliationInternal> internalRows(Long id) {
return internalMapper.selectList(Wrappers.<TransportReconciliationInternal>lambdaQuery().eq(TransportReconciliationInternal::getReconciliationId, id).orderByAsc(TransportReconciliationInternal::getLineNo));
}
private List<TransportReconciliationExternal> externalRows(Long id) {
return externalMapper.selectList(Wrappers.<TransportReconciliationExternal>lambdaQuery().eq(TransportReconciliationExternal::getReconciliationId, id).orderByAsc(TransportReconciliationExternal::getExternalLineNo));
}
private boolean hasMultipleCargo(Long formalDetailId) {
return formalDetailFeeMapper.selectCount(Wrappers.<FormalSettlementDetailFee>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<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumExternalQuantity(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumInternalAmount(List<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumExternalAmount(List<TransportReconciliationExternal> 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")); }
}
@@ -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<TransportReconciliation, TransportReconciliationVO> {
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;
}
}
@@ -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;
@@ -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;