diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java index 6eb8f18..7e5c842 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -29,6 +29,7 @@ public class FormalSettlementSaveRequest implements Serializable { private String attachmentsJson; private String remark; private List summaryFees; + private List invoices; @Data public static class SummaryFee implements Serializable { @@ -40,4 +41,17 @@ public class FormalSettlementSaveRequest implements Serializable { private String remark; private Integer manualFlag; } + + @Data + public static class Invoice implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private String invoiceNo; + private LocalDate invoiceDate; + private String invoiceType; + private BigDecimal taxRate; + private BigDecimal invoiceAmount; + private BigDecimal availableInvoiceAmount; + private BigDecimal matchedAmount; + private String attachmentJson; + } } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationRecordRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationRecordRequest.java new file mode 100644 index 0000000..86827ac --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationRecordRequest.java @@ -0,0 +1,38 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * 1. This software is for development use only under a valid license from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments from this software for such purposes. + * Copyright of this software remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** 付款申请付款记录请求。 @author Chill */ +@Data +public class PaymentApplicationRecordRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private BigDecimal paidAmount; + private LocalDate paidDate; + private String paymentNo; + private String voucherJson; + private String kingdeeBillNo; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java index 498de38..b24df78 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PaymentApplicationSaveRequest.java @@ -58,4 +58,5 @@ public class PaymentApplicationSaveRequest implements Serializable { private String attachmentsJson; private String remark; private List invoices; + private List paymentRecords; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java new file mode 100644 index 0000000..b3834bc --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementChangeRecord.java @@ -0,0 +1,31 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** 正式结算变更记录。 @author Chill */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_change_record") +public class FormalSettlementChangeRecord extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private String changeType; + private Integer lineNo; + private String operationType; + private String changeContent; + private String changeReason; + private String operatorName; + private LocalDateTime changeTime; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java new file mode 100644 index 0000000..ad5c626 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementInvoice.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * 正式结算发票明细实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_invoice") +@Schema(description = "正式结算发票明细") +public class FormalSettlementInvoice extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Integer lineNo; + private String invoiceNo; + private LocalDate invoiceDate; + private String invoiceType; + private BigDecimal taxRate; + private BigDecimal invoiceAmount; + private BigDecimal availableInvoiceAmount; + private BigDecimal matchedAmount; + private String attachmentJson; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index ba68b38..8d636c5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -12,7 +12,9 @@ import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementChangeRecord; import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementInvoice; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.FormalSettlementPayment; import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; @@ -42,4 +44,8 @@ public class FormalSettlementVO extends FormalSettlement { @TableField(exist = false) private List details; @TableField(exist = false) private List summaryFees; @TableField(exist = false) private List payments; + @TableField(exist = false) private List invoices; + @TableField(exist = false) private List paymentApplications; + @TableField(exist = false) private List adjustments; + @TableField(exist = false) private List changeRecords; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java new file mode 100644 index 0000000..367ae47 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationReferenceAmountVO.java @@ -0,0 +1,30 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 付款申请关联结算单金额视图 + * + * @author Chill + */ +@Data +@Schema(description = "付款申请关联结算单金额") +public class PaymentApplicationReferenceAmountVO implements Serializable { + @Serial private static final long serialVersionUID = 1L; + @Schema(description = "结算金额") private BigDecimal settlementAmount; + @Schema(description = "累计申请付款金额") private BigDecimal cumulativeAppliedAmount; + @Schema(description = "可付款金额") private BigDecimal payableAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java index 3f00b3a..ccbb245 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/SettlementAdjustmentVO.java @@ -17,6 +17,7 @@ public class SettlementAdjustmentVO extends SettlementAdjustment { private String approvalStatusName; private String settlementTypeName; private String createUserName; + private String kingdeeBillNo; private List details; private List formalDetails; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java index 575bf17..34bc11d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PaymentApplicationController.java @@ -31,6 +31,7 @@ import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; +import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.pojo.vo.PaymentApplicationVO; import org.springblade.transport.service.IPaymentApplicationService; import org.springframework.web.bind.annotation.GetMapping; @@ -56,32 +57,39 @@ public class PaymentApplicationController extends BladeController { @ApiOperationSupport(order = 2) @Operation(summary = "付款申请详情") public R detail(@RequestParam Long id) { return R.data(paymentApplicationService.detail(id)); } - @PostMapping("/save") + @GetMapping("/reference-amount") @ApiOperationSupport(order = 3) + @Operation(summary = "动态计算结算单可付款金额") + public R referenceAmount(@RequestParam String paymentType, + @RequestParam Long referenceId, @RequestParam(required = false) Long excludeId) { + return R.data(paymentApplicationService.referenceAmount(paymentType, referenceId, excludeId)); + } + @PostMapping("/save") + @ApiOperationSupport(order = 4) @Operation(summary = "保存付款申请") public R save(@RequestBody PaymentApplicationSaveRequest request) { return R.data(paymentApplicationService.saveDraft(request)); } @PostMapping("/remove") - @ApiOperationSupport(order = 4) + @ApiOperationSupport(order = 5) @Operation(summary = "删除付款申请草稿") public R remove(@RequestParam Long id) { paymentApplicationService.removeDraft(id); return R.success("删除成功"); } @PostMapping("/submit") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 6) @Operation(summary = "提交付款申请") public R submit(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.submit(request); return R.success("提交成功"); } @PostMapping("/approve") - @ApiOperationSupport(order = 6) + @ApiOperationSupport(order = 7) @Operation(summary = "审批通过付款申请") public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); } @PostMapping("/return") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 8) @Operation(summary = "驳回付款申请") public R returnBill(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.returnBill(request); return R.success("已驳回"); } @PostMapping("/void") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 9) @Operation(summary = "作废付款申请") public R voidBill(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.voidBill(request); return R.success("作废成功"); } @PostMapping("/sync-kingdee") - @ApiOperationSupport(order = 9) + @ApiOperationSupport(order = 10) @Operation(summary = "生成金蝶付款单") public R syncKingdee(@RequestParam Long id) { return R.data(paymentApplicationService.syncKingdee(id)); } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementChangeRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementChangeRecordMapper.java new file mode 100644 index 0000000..a9de094 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementChangeRecordMapper.java @@ -0,0 +1,16 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlementChangeRecord; + +/** 正式结算变更记录 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementChangeRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementInvoiceMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementInvoiceMapper.java new file mode 100644 index 0000000..78969cd --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementInvoiceMapper.java @@ -0,0 +1,22 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.transport.pojo.entity.FormalSettlementInvoice; + +/** + * 正式结算发票明细 Mapper + * + * @author Chill + */ +@Mapper +public interface FormalSettlementInvoiceMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java index 7a9c0f4..2ba2efe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java @@ -24,12 +24,14 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; import org.springblade.transport.pojo.entity.PaymentApplication; +import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.pojo.vo.PaymentApplicationVO; /** 付款申请服务。 @author Chill */ public interface IPaymentApplicationService extends BaseService { IPage selectPage(IPage page, PaymentApplicationVO query); PaymentApplicationVO detail(Long id); + PaymentApplicationReferenceAmountVO referenceAmount(String paymentType, Long referenceId, Long excludeId); Long saveDraft(PaymentApplicationSaveRequest request); void removeDraft(Long id); void submit(PaymentApplicationStatusRequest request); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java index 3181c02..6fd8851 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -19,7 +19,9 @@ import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; +import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper; import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementInvoiceMapper; import org.springblade.transport.mapper.FormalSettlementSourceMapper; import org.springblade.transport.mapper.FormalSettlementSummaryFeeMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; @@ -27,15 +29,19 @@ import org.springblade.transport.mapper.PreSettlementDetailMapper; import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; import org.springblade.transport.mapper.PreSettlementMapper; import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; +import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +import org.springblade.transport.mapper.SettlementAdjustmentMapper; import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementChangeRecord; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.FormalSettlementInvoice; import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.FormalSettlementSummaryFee; import org.springblade.transport.pojo.entity.FormalSettlementPayment; @@ -44,11 +50,14 @@ import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; +import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; +import org.springblade.transport.pojo.entity.SettlementAdjustment; import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.FormalSettlementVO; import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.pojo.vo.SettlementAdjustmentVO; import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.IPreSettlementService; @@ -56,6 +65,8 @@ import org.springblade.transport.service.IWaybillService; import org.springblade.transport.wrapper.PreSettlementWrapper; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.transport.wrapper.FormalSettlementWrapper; +import org.springblade.transport.wrapper.PaymentApplicationWrapper; +import org.springblade.system.cache.UserCache; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -91,6 +102,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() - .eq(FormalSettlementSource::getFormalSettlementId, id).orderByAsc(FormalSettlementSource::getCreateTime))); + List sources = sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, id).orderByAsc(FormalSettlementSource::getCreateTime)); + vo.setSources(sources); vo.setDetails(detailMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo))); vo.setSummaryFees(listSummaryFees(id)); vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime))); + vo.setInvoices(invoiceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, id).orderByAsc(FormalSettlementInvoice::getLineNo))); + List preSettlementIds = sources.stream().map(FormalSettlementSource::getPreSettlementId).toList(); + LambdaQueryWrapper paymentApplicationQuery = Wrappers.lambdaQuery(); + if (preSettlementIds.isEmpty()) { + paymentApplicationQuery.eq(PaymentApplication::getSettlementId, id); + } else { + paymentApplicationQuery.and(query -> query.eq(PaymentApplication::getSettlementId, id) + .or().in(PaymentApplication::getPreSettlementId, preSettlementIds)); + } + vo.setPaymentApplications(paymentApplicationMapper.selectList(paymentApplicationQuery + .orderByDesc(PaymentApplication::getCreateTime)).stream() + .map(item -> PaymentApplicationWrapper.build().entityVO(item)).toList()); + vo.setAdjustments(settlementAdjustmentMapper.selectList(Wrappers.lambdaQuery() + .eq(SettlementAdjustment::getFormalSettlementId, id) + .orderByDesc(SettlementAdjustment::getCreateTime)).stream() + .map(item -> toAdjustmentVO(item, settlement.getKingdeeBillNo())).toList()); + vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementChangeRecord::getFormalSettlementId, id) + .orderByDesc(FormalSettlementChangeRecord::getChangeTime))); return vo; } @@ -189,6 +225,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl sources = Func.isEmpty(request.getSourcePreSettlementIds()) ? List.of() : request.getSourcePreSettlementIds().stream().distinct().map(this::availableSource).toList(); @@ -241,6 +278,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, id)); summaryFeeMapper.delete(Wrappers.lambdaQuery() .eq(FormalSettlementSummaryFee::getFormalSettlementId, id)); + invoiceMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, id)); + changeRecordMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementChangeRecord::getFormalSettlementId, id)); removeById(id); } @@ -271,6 +315,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl existing = detailFees(detail.getId()).stream() .collect(Collectors.toMap(FormalSettlementDetailFee::getId, item -> item)); if (existing.size() != request.getRows().size()) throw new ServiceException("费用调整行数据不完整"); + List changes = new java.util.ArrayList<>(); for (PreSettlementDetailAdjustRequest.FeeRow row : request.getRows()) { FormalSettlementDetailFee fee = existing.get(row.getId()); if (fee == null) throw new ServiceException("存在无效的货物费用行"); + BigDecimal beforeAmount = money(fee.getSettlementAmountTax()); fee.setTransportQuantity(nonNegative(row.getTransportQuantity(), "运输总量")); fee.setMileage(nonNegative(row.getMileage(), "里程")); fee.setUnitPrice(nonNegative(row.getUnitPrice(), "运输单价")); @@ -353,6 +402,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl rows = detailFees(detail.getId()); detail.setTransportQuantity(rows.stream().map(FormalSettlementDetailFee::getTransportQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); @@ -365,6 +419,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl requestRows) { + List invoices = requestRows == null ? List.of() : requestRows; + Set invoiceNumbers = new LinkedHashSet<>(); + BigDecimal matchedTotal = BigDecimal.ZERO; + for (FormalSettlementSaveRequest.Invoice requestRow : invoices) { + String invoiceNo = requiredText(requestRow.getInvoiceNo(), "发票号"); + if (invoiceNo.length() > 32) throw new ServiceException("发票号不能超过32个字"); + if (!invoiceNumbers.add(invoiceNo)) throw new ServiceException("发票号" + invoiceNo + "重复"); + BigDecimal invoiceAmount = nonNegative(requestRow.getInvoiceAmount(), "发票金额"); + BigDecimal availableAmount = nonNegative(requestRow.getAvailableInvoiceAmount(), "可匹配发票金额"); + BigDecimal matchedAmount = nonNegative(requestRow.getMatchedAmount(), "匹配结算单金额"); + if (availableAmount.compareTo(invoiceAmount) > 0) { + throw new ServiceException("发票" + invoiceNo + "的可匹配金额不能超过发票金额"); + } + if (matchedAmount.compareTo(availableAmount) > 0) { + throw new ServiceException("发票" + invoiceNo + "的匹配结算单金额不能超过可匹配金额"); + } + if (requestRow.getTaxRate() != null && (requestRow.getTaxRate().signum() < 0 + || requestRow.getTaxRate().compareTo(BigDecimal.valueOf(100)) > 0)) { + throw new ServiceException("发票" + invoiceNo + "的税率必须在0-100之间"); + } + matchedTotal = matchedTotal.add(matchedAmount); + } + if (matchedTotal.compareTo(money(settlement.getSettlementAmount())) > 0) { + throw new ServiceException("发票匹配结算单金额合计不能超过结算金额"); + } + invoiceMapper.delete(Wrappers.lambdaQuery() + .eq(FormalSettlementInvoice::getFormalSettlementId, settlement.getId())); + int lineNo = 1; + for (FormalSettlementSaveRequest.Invoice requestRow : invoices) { + FormalSettlementInvoice invoice = new FormalSettlementInvoice(); + invoice.setFormalSettlementId(settlement.getId()); + invoice.setLineNo(lineNo++); + invoice.setInvoiceNo(requestRow.getInvoiceNo().trim()); + invoice.setInvoiceDate(requestRow.getInvoiceDate()); + invoice.setInvoiceType(limit(requestRow.getInvoiceType(), 50)); + invoice.setTaxRate(requestRow.getTaxRate()); + invoice.setInvoiceAmount(money(requestRow.getInvoiceAmount())); + invoice.setAvailableInvoiceAmount(money(requestRow.getAvailableInvoiceAmount())); + invoice.setMatchedAmount(money(requestRow.getMatchedAmount())); + invoice.setAttachmentJson(requestRow.getAttachmentJson()); + invoiceMapper.insert(invoice); + } + } + private void refreshSettlementAmount(FormalSettlement settlement) { BigDecimal amount = listSummaryFees(settlement.getId()).stream() .map(FormalSettlementSummaryFee::getSettlementAmount).map(this::money) @@ -707,6 +808,27 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl "草稿"; + case REVIEWING -> "审批中"; + case APPROVED -> "审批通过"; + case RETURNED -> "已驳回"; + case VOIDED -> "已作废"; + default -> value; + }; + } + private PreSettlement availableSource(Long id) { PreSettlement source = preSettlementMapper.selectById(id); if (source == null || Objects.equals(source.getIsDeleted(), 1)) throw new ServiceException("预结算单不存在"); @@ -742,6 +864,21 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl max) throw new ServiceException("内容不能超过" + max + "个字"); return value; } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 3ac1348..842e01a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -35,10 +35,13 @@ import org.springblade.transport.mapper.BillLedgerUsageMapper; import org.springblade.transport.mapper.PreSettlementMapper; import org.springblade.transport.mapper.ProjectApplyMapper; import org.springblade.transport.mapper.CustomerArchiveMapper; +import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper; import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.PaymentApplicationRecordMapper; +import org.springblade.transport.mapper.TemporaryCreditLimitMapper; import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest; +import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest; import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; import org.springblade.transport.pojo.entity.FormalSettlement; @@ -47,9 +50,13 @@ import org.springblade.transport.pojo.entity.BillLedgerUsage; import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; +import org.springblade.transport.pojo.entity.PaymentApplicationRecord; +import org.springblade.transport.pojo.entity.TemporaryCreditLimit; import org.springblade.transport.pojo.vo.PaymentApplicationVO; +import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; import org.springblade.transport.service.IPaymentApplicationService; import org.springblade.transport.wrapper.PaymentApplicationWrapper; import org.springframework.stereotype.Service; @@ -61,7 +68,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.stream.Stream; /** 付款申请服务实现。 @author Chill */ @Service @@ -78,9 +87,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl selectPage(IPage page, PaymentApplicationVO query) { @@ -114,6 +125,39 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl invoiceRequests = ("project_advance".equals(request.getPaymentType()) + || request.getInvoices() == null) ? List.of() : request.getInvoices(); + BigDecimal matchedInvoiceAmount = BigDecimal.ZERO; + for (PaymentApplicationInvoiceRequest item : invoiceRequests) { + validateInvoice(item); + matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount())); + } + entity.setMatchedInvoiceAmount(matchedInvoiceAmount); fillReference(entity, request); fillBillLedger(entity, request); validateQuota(entity); @@ -146,27 +198,38 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId())); int lineNo = 1; - BigDecimal matchedInvoiceAmount = BigDecimal.ZERO; - for (PaymentApplicationInvoiceRequest item : request.getInvoices() == null ? List.of() : request.getInvoices()) { - validateInvoice(item); + for (PaymentApplicationInvoiceRequest item : invoiceRequests) { PaymentApplicationInvoice invoice = Objects.requireNonNull(BeanUtil.copyProperties(item, PaymentApplicationInvoice.class)); invoice.setId(null); invoice.setPaymentApplicationId(entity.getId()); invoice.setLineNo(lineNo++); invoiceMapper.insert(invoice); - matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount())); } - if (matchedInvoiceAmount.compareTo(money(entity.getAppliedAmount())) > 0) { - throw new ServiceException("发票匹配金额合计不能超过申请付款金额"); + recordMapper.delete(Wrappers.lambdaQuery() + .eq(PaymentApplicationRecord::getPaymentApplicationId, entity.getId())); + BigDecimal paidAmount = BigDecimal.ZERO; + List recordRequests = ("project_advance".equals(request.getPaymentType()) + || request.getPaymentRecords() == null) ? List.of() : request.getPaymentRecords(); + for (PaymentApplicationRecordRequest item : recordRequests) { + validatePaymentRecord(item); + PaymentApplicationRecord record = Objects.requireNonNull(BeanUtil.copyProperties(item, PaymentApplicationRecord.class)); + record.setId(null); + record.setPaymentApplicationId(entity.getId()); + recordMapper.insert(record); + paidAmount = paidAmount.add(money(item.getPaidAmount())); + } + if (paidAmount.compareTo(money(entity.getAppliedAmount())) > 0) { + throw new ServiceException("付款记录金额合计不能超过申请付款金额"); } entity.setMatchedInvoiceAmount(matchedInvoiceAmount); entity.setInvoiceStatus(matchedInvoiceAmount.compareTo(BigDecimal.ZERO) > 0 ? "matched" : "unmatched"); + entity.setPaidAmount(paidAmount); updateById(entity); return entity.getId(); } @Override @Transactional(rollbackFor = Exception.class) - public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); removeById(entity); } + public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); removeById(entity); } @Override @Transactional(rollbackFor = Exception.class) public void submit(PaymentApplicationStatusRequest request) { @@ -174,7 +237,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 100) { + throw new ServiceException("结算单号不能超过100个字符"); + } + if (invoice.getInvoiceNo() != null && invoice.getInvoiceNo().length() > 100) { + throw new ServiceException("发票号不能超过100个字符"); + } + if (invoice.getInvoiceType() != null && invoice.getInvoiceType().length() > 30) { + throw new ServiceException("发票类型不能超过30个字符"); + } BigDecimal invoiceAmount = money(invoice.getInvoiceAmount()); BigDecimal matchedAmount = money(invoice.getMatchedAmount()); if (invoiceAmount.compareTo(BigDecimal.ZERO) < 0) { @@ -245,18 +320,66 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 100) { + throw new ServiceException("付款单号不能超过100个字符"); + } + if (record.getKingdeeBillNo() != null && record.getKingdeeBillNo().length() > 100) { + throw new ServiceException("付款凭证不能超过100个字符"); + } + } + + private void validateProjectAdvanceContractFile(PaymentApplication entity) { + if (!"project_advance".equals(entity.getPaymentType())) return; + ContractManage contract = entity.getContractId() == null ? null : contractManageMapper.selectById(entity.getContractId()); + if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) { + throw new ServiceException("项目预付必须选择有效合同"); + } + if (!hasDownloadableFile(contract.getContractFileJson()) + && !hasDownloadableFile(entity.getAttachmentsJson(), "contract")) { + throw new ServiceException("请上传合同签章文件"); + } + } + + private boolean hasDownloadableFile(String value) { + return hasDownloadableFile(value, null); + } + + private boolean hasDownloadableFile(String value, String attachmentType) { + if (Func.isEmpty(value)) return false; + try { + Object parsed = JsonUtil.parse(value, List.class); + if (!(parsed instanceof List files)) return false; + return files.stream().anyMatch(file -> file instanceof Map fields + && (attachmentType == null || Objects.equals(attachmentType, fields.get("attachmentType"))) + && List.of("url", "link", "fileUrl", "downloadUrl", "src", "domain").stream() + .anyMatch(key -> Func.isNotEmpty(fields.get(key)))); + } catch (Exception exception) { + return false; + } + } + private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) { - if (request.getSettlementId() != null) { + if ("project_advance".equals(request.getPaymentType())) { + ProjectApply project = projectApplyMapper.selectById(request.getProjectId()); + if (project == null || Objects.equals(project.getIsDeleted(), 1)) throw new ServiceException("所属项目不存在"); + entity.setSettlementId(null); entity.setSettlementNo(null); entity.setPreSettlementId(null); entity.setPreSettlementNo(null); + entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(money(project.getFundDemand()).multiply(BigDecimal.valueOf(10000))); entity.setBillType(Func.isEmpty(project.getSettlementMode()) ? null : "项目预付"); + } else if (request.getSettlementId() != null) { FormalSettlement source = formalSettlementMapper.selectById(request.getSettlementId()); if (source == null) throw new ServiceException("正式结算单不存在"); if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单"); entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null); - entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(money(source.getAppliedPaymentAmount()))); entity.setBillType("正式结算单"); + entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativeAppliedAmount(request.getPaymentType(), source.getId(), entity.getId()))); entity.setBillType("正式结算单"); } else if (request.getPreSettlementId() != null) { PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId()); if (source == null) throw new ServiceException("预结算单不存在"); if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单"); - entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(money(source.getAdvanceAppliedAmount()))); entity.setBillType("预结算单"); + entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativeAppliedAmount(request.getPaymentType(), source.getId(), entity.getId()))); entity.setBillType("预结算单"); } else { entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType()); } @@ -264,32 +387,73 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() - .eq(entity.getProjectId() != null, PaymentApplication::getProjectId, entity.getProjectId()) - .ne(entity.getId() != null, PaymentApplication::getId, entity.getId())); - ProjectApply project = entity.getProjectId() == null ? null : projectApplyMapper.selectById(entity.getProjectId()); + ProjectApply project = entity.getProjectId() == null ? null : projectApplyMapper.selectOne( + Wrappers.lambdaQuery().eq(ProjectApply::getId, entity.getProjectId()).last("FOR UPDATE")); if (project != null && project.getFundLimit() != null) { - BigDecimal projectLimit = project.getFundLimit().multiply(BigDecimal.valueOf(10000)); - if (usedByProject.add(money(entity.getAppliedAmount())).compareTo(projectLimit) > 0) throw new ServiceException("申请付款金额超过项目剩余资金使用额度"); + BigDecimal projectLimit = projectFundLimit(project); + BigDecimal projectOccupiedAmount = quotaOccupiedAmount(Wrappers.lambdaQuery() + .eq(PaymentApplication::getProjectId, entity.getProjectId()) + .ne(entity.getId() != null, PaymentApplication::getId, entity.getId()), entity); + if (projectOccupiedAmount.compareTo(projectLimit) > 0) { + throw new ServiceException("申请付款金额超过项目剩余资金使用额度(已扣减收票金额并包含在途申请)"); + } } if (Func.isNotEmpty(entity.getPayeeName())) { CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.lambdaQuery() .and(wrapper -> wrapper.eq(CustomerArchive::getFullName, entity.getPayeeName()).or().eq(CustomerArchive::getShortName, entity.getPayeeName())) - .eq(CustomerArchive::getIsDeleted, 0).last("limit 1")); + .eq(CustomerArchive::getIsDeleted, 0).last("limit 1 FOR UPDATE")); if (customer != null && customer.getMaxCreditLimit() != null) { BigDecimal customerLimit = customer.getMaxCreditLimit().multiply(BigDecimal.valueOf(10000)); - BigDecimal usedByCustomer = sumApplied(Wrappers.lambdaQuery() - .eq(PaymentApplication::getPayeeName, entity.getPayeeName()).ne(entity.getId() != null, PaymentApplication::getId, entity.getId())); - if (usedByCustomer.add(money(entity.getAppliedAmount())).compareTo(customerLimit) > 0) throw new ServiceException("申请付款金额超过客户剩余资金使用额度"); + List customerNames = Stream.of(customer.getFullName(), customer.getShortName()) + .filter(Func::isNotEmpty).map(String::trim).distinct().toList(); + BigDecimal customerOccupiedAmount = quotaOccupiedAmount(Wrappers.lambdaQuery() + .in(PaymentApplication::getPayeeName, customerNames) + .ne(entity.getId() != null, PaymentApplication::getId, entity.getId()), entity); + if (customerOccupiedAmount.compareTo(customerLimit) > 0) { + throw new ServiceException("申请付款金额超过客户剩余资金使用额度(已扣减收票金额并包含在途申请)"); + } } } } + private BigDecimal projectFundLimit(ProjectApply project) { + BigDecimal temporaryLimit = temporaryCreditLimitMapper.selectList(Wrappers.lambdaQuery() + .eq(TemporaryCreditLimit::getProjectId, project.getId()) + .eq(TemporaryCreditLimit::getApprovalStatus, APPROVED) + .ge(TemporaryCreditLimit::getValidUntil, LocalDate.now()) + .eq(TemporaryCreditLimit::getStatus, 1) + .eq(TemporaryCreditLimit::getIsDeleted, 0)).stream() + .map(TemporaryCreditLimit::getApplyLimit).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + return money(project.getFundLimit()).add(temporaryLimit).multiply(BigDecimal.valueOf(10000)); + } + + private BigDecimal quotaOccupiedAmount(LambdaQueryWrapper wrapper, + PaymentApplication currentApplication) { + List applications = list(wrapper.ne(PaymentApplication::getApprovalStatus, VOIDED) + .last("FOR UPDATE")); + BigDecimal appliedAmount = applications.stream().map(PaymentApplication::getAppliedAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add) + .add(money(currentApplication.getAppliedAmount())); + BigDecimal receivedInvoiceAmount = applications.stream().map(PaymentApplication::getMatchedInvoiceAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add) + .add(money(currentApplication.getMatchedInvoiceAmount())); + return appliedAmount.subtract(receivedInvoiceAmount).max(BigDecimal.ZERO); + } + private BigDecimal sumApplied(LambdaQueryWrapper wrapper) { return list(wrapper.ne(PaymentApplication::getApprovalStatus, VOIDED)).stream() .map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } + private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq("settlement_payment".equals(paymentType), PaymentApplication::getSettlementId, referenceId) + .eq("progress_advance".equals(paymentType), PaymentApplication::getPreSettlementId, referenceId) + .ne(excludeId != null, PaymentApplication::getId, excludeId); + return sumApplied(wrapper); + } + private void fillBillLedger(PaymentApplication entity, PaymentApplicationSaveRequest request) { if (!isBillPayment(request.getPaymentMethod())) { entity.setBillLedgerId(null); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index e190045..4a22664 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -56,6 +56,7 @@ import org.springblade.transport.pojo.entity.PreSettlementChangeRecord; import org.springblade.transport.pojo.entity.PreSettlementDetail; import org.springblade.transport.pojo.entity.PreSettlementDetailFee; import org.springblade.transport.pojo.entity.PreSettlementSummaryFee; +import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; import org.springblade.transport.pojo.entity.ReceivablePayableDetail; import org.springblade.transport.pojo.entity.Waybill; @@ -183,7 +184,15 @@ public class PreSettlementServiceImpl extends BaseServiceImpl query.like(ContractManage::getContractName, keyword) .or().like(ContractManage::getContractNo, keyword)) .orderByDesc(ContractManage::getCreateTime); - return contractManageService.list(wrapper).stream().filter(this::isAvailableContract).map(contract -> { + List contracts = contractManageService.list(wrapper).stream() + .filter(this::isAvailableContract).toList(); + Set projectIds = contracts.stream().map(ContractManage::getProjectId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + Map projectById = Func.isEmpty(projectIds) ? Map.of() + : projectApplyService.listByIds(projectIds).stream() + .collect(Collectors.toMap(ProjectApply::getId, Function.identity())); + return contracts.stream().map(contract -> { + ProjectApply project = projectById.get(contract.getProjectId()); Map result = new LinkedHashMap<>(); result.put("id", contract.getId()); result.put("contractNo", contract.getContractNo()); @@ -194,6 +203,8 @@ public class PreSettlementServiceImpl extends BaseServiceImpl selectPage(IPage page, SettlementAdjustmentVO query) { @@ -178,9 +181,15 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpllambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId()))) { FormalSettlementDetailFee fee = validateFee(formal.getId(), item.getFormalSettlementDetailId(), item.getFormalSettlementDetailFeeId()); + BigDecimal beforeAmount = money(fee.getSettlementAmountTax()); 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); + FormalSettlementDetail formalDetail = formalDetailMapper.selectById(item.getFormalSettlementDetailId()); + saveFormalChange(formal.getId(), "结算明细项", formalDetail == null ? null : formalDetail.getLineNo(), + "调整", "【" + (Func.isEmpty(item.getFeeItem()) ? "结算费用" : item.getFeeItem()) + + "】从【" + beforeAmount + "】调整为【" + fee.getSettlementAmountTax() + "】", + Func.isEmpty(item.getRemark()) ? adjustment.getRemark() : item.getRemark()); } for (FormalSettlementDetail detail : formalDetailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, formal.getId()))) { List fees = formalFeeMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())); @@ -190,9 +199,26 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpllambdaQuery().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); + saveFormalChange(formal.getId(), "合计费用项", null, "调整", + "调整单" + adjustment.getAdjustmentNo() + "调整金额【" + money(adjustment.getAdjustmentAmount()) + "】", + adjustment.getRemark()); adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment); } + private void saveFormalChange(Long settlementId, String changeType, Integer lineNo, + String operationType, String content, String reason) { + FormalSettlementChangeRecord record = new FormalSettlementChangeRecord(); + record.setFormalSettlementId(settlementId); + record.setChangeType(changeType); + record.setLineNo(lineNo); + record.setOperationType(operationType); + record.setChangeContent(content); + record.setChangeReason(reason); + record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); + record.setChangeTime(LocalDateTime.now()); + formalChangeRecordMapper.insert(record); + } + 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; } diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql index 01da282..4c44590 100644 --- a/doc/sql/transport/blade_formal_settlement_20260818.sql +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -23,6 +23,33 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement` ( KEY `idx_formal_contract` (`contract_id`), KEY `idx_formal_approval` (`approval_status`), KEY `idx_formal_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算单'; +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_invoice` ( + `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `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', + `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', `line_no` int(11) NOT NULL COMMENT '行号', + `invoice_no` varchar(32) NOT NULL COMMENT '发票号', `invoice_date` date DEFAULT NULL COMMENT '开票日期', + `invoice_type` varchar(50) DEFAULT NULL COMMENT '发票类型', `tax_rate` decimal(8,4) DEFAULT NULL COMMENT '税率', + `invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '发票金额(含税)', + `available_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '可匹配发票金额(含税)', + `matched_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '匹配结算单金额(含税)', + `attachment_json` longtext COMMENT '发票附件JSON', PRIMARY KEY (`id`), + UNIQUE KEY `uk_formal_settlement_invoice_no` (`tenant_id`,`formal_settlement_id`,`invoice_no`), + KEY `idx_formal_settlement_invoice` (`formal_settlement_id`,`line_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算发票明细'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_change_record` ( + `id` bigint(20) NOT NULL COMMENT '主键', `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `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', + `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', `change_type` varchar(50) NOT NULL COMMENT '变更类型', + `line_no` int(11) DEFAULT NULL COMMENT '行号', `operation_type` varchar(30) NOT NULL COMMENT '操作类型', + `change_content` text NOT NULL COMMENT '变更内容', `change_reason` varchar(200) DEFAULT NULL COMMENT '变更原因', + `operator_name` varchar(100) DEFAULT NULL COMMENT '操作人', `change_time` datetime NOT NULL COMMENT '变更时间', + PRIMARY KEY (`id`), KEY `idx_formal_settlement_change_bill` (`formal_settlement_id`), + KEY `idx_formal_settlement_change_time` (`change_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算变更记录'; + CREATE TABLE IF NOT EXISTS `blade_formal_settlement_source` ( `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, diff --git a/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql b/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql new file mode 100644 index 0000000..08ba660 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_change_record_20260827.sql @@ -0,0 +1,23 @@ +-- 正式结算详情增加变更记录模块。 +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_change_record` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', + `change_type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '变更类型', + `line_no` int(11) DEFAULT NULL COMMENT '行号', + `operation_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL COMMENT '操作类型', + `change_content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '变更内容', + `change_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因', + `operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人', + `change_time` datetime NOT NULL COMMENT '变更时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_formal_settlement_change_bill` (`formal_settlement_id`) USING BTREE, + KEY `idx_formal_settlement_change_time` (`change_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算变更记录'; diff --git a/doc/sql/transport/blade_formal_settlement_invoice_20260827.sql b/doc/sql/transport/blade_formal_settlement_invoice_20260827.sql new file mode 100644 index 0000000..016d596 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_invoice_20260827.sql @@ -0,0 +1,26 @@ +-- 正式结算单增加认领发票明细 + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_invoice` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否删除', + `formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `invoice_no` varchar(32) NOT NULL COMMENT '发票号', + `invoice_date` date DEFAULT NULL COMMENT '开票日期', + `invoice_type` varchar(50) DEFAULT NULL COMMENT '发票类型', + `tax_rate` decimal(8,4) DEFAULT NULL COMMENT '税率', + `invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '发票金额(含税)', + `available_invoice_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '可匹配发票金额(含税)', + `matched_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '匹配结算单金额(含税)', + `attachment_json` longtext COMMENT '发票附件JSON', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_formal_settlement_invoice_no` (`tenant_id`,`formal_settlement_id`,`invoice_no`), + KEY `idx_formal_settlement_invoice` (`formal_settlement_id`,`line_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算发票明细'; diff --git a/doc/sql/transport/blade_payment_application_quota_index_20260827.sql b/doc/sql/transport/blade_payment_application_quota_index_20260827.sql new file mode 100644 index 0000000..d7cf6ab --- /dev/null +++ b/doc/sql/transport/blade_payment_application_quota_index_20260827.sql @@ -0,0 +1,3 @@ +ALTER TABLE `blade_payment_application` + ADD KEY `idx_payment_application_project_quota` (`tenant_id`, `project_id`, `is_deleted`, `approval_status`), + ADD KEY `idx_payment_application_payee_quota` (`tenant_id`, `payee_name`(100), `is_deleted`, `approval_status`);