From 154cc4a7cb278e2e87964ea7b0c2d02ef8a71eec Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 18 Aug 2026 21:31:59 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9E=E9=A2=84=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=8D=95=202=E3=80=81=E6=96=B0=E5=A2=9E=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=E7=BB=93=E7=AE=97=E5=8D=95=203=E3=80=81=E8=B0=83?= =?UTF-8?q?=E6=95=B4IAM=E8=AE=A4=E8=AF=81=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/config/BladeAuthConfiguration.java | 15 + .../auth/granter/IamSsoTokenGranter.java | 43 +- blade-auth/src/main/resources/application.yml | 13 + .../gateway/provider/AuthProvider.java | 1 + .../springblade/system/feign/ISysClient.java | 9 + .../system/feign/ISysClientFallback.java | 5 + .../dto/FormalSettlementPaymentRequest.java | 23 + .../pojo/dto/FormalSettlementSaveRequest.java | 31 + .../dto/FormalSettlementStatusRequest.java | 21 + .../pojo/dto/PreSettlementAdvanceRequest.java | 40 + .../dto/PreSettlementDetailAdjustRequest.java | 77 + .../pojo/dto/PreSettlementSaveRequest.java | 99 ++ .../pojo/dto/PreSettlementStatusRequest.java | 42 + .../pojo/entity/FormalSettlement.java | 65 + .../pojo/entity/FormalSettlementDetail.java | 59 + .../entity/FormalSettlementDetailFee.java | 45 + .../pojo/entity/FormalSettlementPayment.java | 37 + .../pojo/entity/FormalSettlementSource.java | 35 + .../transport/pojo/entity/PreSettlement.java | 143 ++ .../pojo/entity/PreSettlementAdvance.java | 58 + .../entity/PreSettlementChangeRecord.java | 59 + .../pojo/entity/PreSettlementDetail.java | 117 ++ .../pojo/entity/PreSettlementDetailFee.java | 83 + .../pojo/entity/PreSettlementSummaryFee.java | 62 + .../transport/pojo/vo/FormalSettlementVO.java | 43 + .../transport/pojo/vo/PreSettlementVO.java | 108 ++ .../springblade/system/feign/IUserClient.java | 10 + .../springblade/system/feign/SysClient.java | 12 + .../springblade/system/feign/UserClient.java | 6 + .../system/service/IUserService.java | 8 + .../system/service/impl/UserServiceImpl.java | 30 + .../FormalSettlementController.java | 146 ++ .../controller/PreSettlementController.java | 262 +++ .../transport/excel/PreSettlementExcel.java | 72 + .../FormalSettlementDetailFeeMapper.java | 18 + .../mapper/FormalSettlementDetailMapper.java | 18 + .../mapper/FormalSettlementMapper.java | 18 + .../mapper/FormalSettlementPaymentMapper.java | 18 + .../mapper/FormalSettlementSourceMapper.java | 18 + .../mapper/PreSettlementAdvanceMapper.java | 22 + .../PreSettlementChangeRecordMapper.java | 22 + .../mapper/PreSettlementDetailFeeMapper.java | 22 + .../mapper/PreSettlementDetailMapper.java | 22 + .../transport/mapper/PreSettlementMapper.java | 22 + .../mapper/PreSettlementSummaryFeeMapper.java | 22 + .../service/IFormalSettlementService.java | 43 + .../service/IPreSettlementService.java | 71 + .../impl/FormalSettlementServiceImpl.java | 511 ++++++ .../impl/PreSettlementServiceImpl.java | 1404 +++++++++++++++++ .../wrapper/FormalSettlementWrapper.java | 45 + .../wrapper/PreSettlementWrapper.java | 51 + .../blade_formal_settlement_20260818.sql | 90 ++ .../blade_pre_settlement_20260818.sql | 214 +++ 53 files changed, 4517 insertions(+), 13 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java create mode 100644 doc/sql/transport/blade_formal_settlement_20260818.sql create mode 100644 doc/sql/transport/blade_pre_settlement_20260818.sql diff --git a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java index 15a2061..f587e7e 100644 --- a/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java +++ b/blade-auth/src/main/java/org/springblade/auth/config/BladeAuthConfiguration.java @@ -25,6 +25,7 @@ */ package org.springblade.auth.config; +import org.springblade.auth.granter.IamAwareTokenGranterFactory; import org.springblade.auth.handler.BladeAuthorizationHandler; import org.springblade.auth.handler.BladeLockHandler; import org.springblade.auth.handler.BladeLogHandler; @@ -37,6 +38,9 @@ import org.springblade.core.jwt.props.JwtProperties; import org.springblade.core.launch.props.BladeProperties; import org.springblade.core.launch.server.ServerInfo; import org.springblade.core.oauth2.config.OAuth2AutoConfiguration; +import org.springblade.core.oauth2.granter.TokenGranter; +import org.springblade.core.oauth2.granter.TokenGranterEnhancer; +import org.springblade.core.oauth2.granter.TokenGranterFactory; import org.springblade.core.oauth2.handler.AuthorizationHandler; import org.springblade.core.oauth2.handler.PasswordHandler; import org.springblade.core.oauth2.handler.TokenHandler; @@ -51,8 +55,11 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; +import java.util.List; + /** * BladeAuthConfiguration * @@ -101,4 +108,12 @@ public class BladeAuthConfiguration { return new BladeUserDetailService(userClient); } + @Primary + @Bean("iamAwareTokenGranterFactory") + public TokenGranterFactory tokenGranterFactory(List tokenGranters, + List tokenGranterEnhancers, + OAuth2Properties properties) { + return new IamAwareTokenGranterFactory(tokenGranters, tokenGranterEnhancers, properties); + } + } diff --git a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java index 523fe49..6c6d279 100644 --- a/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java +++ b/blade-auth/src/main/java/org/springblade/auth/granter/IamSsoTokenGranter.java @@ -65,8 +65,10 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; +import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -167,7 +169,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { return result.getData(); } log.info("IAM统一身份认证未匹配到本系统账号,开始自动创建用户,tenantId={}, accountNo={}", tenantId, accountNo); - R saveResult = userClient.saveUser(buildIamUser(profileResponse, tenantId, accountNo)); + R saveResult = userClient.saveIamUser(buildIamUser(profileResponse, tenantId, accountNo)); if (!saveResult.isSuccess() || !Boolean.TRUE.equals(saveResult.getData())) { log.warn("IAM统一身份认证自动创建用户失败,tenantId={}, accountNo={}, msg={}", tenantId, accountNo, saveResult.getMsg()); throw new UserInvalidException(OAuth2TokenConstant.USER_NOT_FOUND); @@ -185,12 +187,14 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { user.setTenantId(tenantId); user.setUserType(UserType.WEB.getCategory()); user.setAccount(accountNo); - user.setPassword(accountNo); + user.setPassword(UUID.randomUUID().toString()); user.setName(accountNo); user.setRealName(accountNo); user.setRoleId(StringPool.MINUS_ONE); user.setDeptId(StringPool.MINUS_ONE); user.setPostId(StringPool.MINUS_ONE); + user.setIsOa(1); + user.setSyncTime(new Date()); user.setStatus(StatusType.ACTIVE.getType()); log.info("IAM统一身份认证自动创建用户参数,tenantId={}, accountNo={}, iamId={}", tenantId, accountNo, profileResponse.getId()); return user; @@ -206,13 +210,13 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { } private boolean isIamRequest(OAuth2Request request, boolean logMiss) { - String redirectUri = normalizeRedirectUri(request.getRedirectUri()); - boolean iamRequest = StringUtil.isNotBlank(properties.getRedirectUri()) + String redirectUri = resolveRedirectUri(request); + boolean iamRequest = StringUtil.isNotBlank(redirectUri) && StringUtil.isNotBlank(request.getCode()) - && StringUtil.equals(properties.getRedirectUri(), redirectUri); + && isDedicatedIamEndpoint(request); if (!iamRequest && logMiss) { - log.info("IAM统一身份认证请求未命中,configRedirectUri={}, requestRedirectUri={}, normalizedRedirectUri={}, code={}", - properties.getRedirectUri(), request.getRedirectUri(), redirectUri, request.getCode()); + log.info("IAM统一身份认证请求未命中,configRedirectUri={}, requestRedirectUri={}, normalizedRedirectUri={}, hasCode={}", + properties.getRedirectUri(), request.getRedirectUri(), redirectUri, StringUtil.isNotBlank(request.getCode())); } return iamRequest; } @@ -247,8 +251,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { properties.getTokenUrl(), properties.getProfileUrl(), properties.getClientId(), - properties.getClientSecret(), - properties.getRedirectUri() + properties.getClientSecret() )) { throw new UserInvalidException("IAM统一身份认证配置不完整"); } @@ -265,7 +268,7 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { .header(HttpHeaders.AUTHORIZATION, authorizationHeader()) .POST(HttpRequest.BodyPublishers.ofString(tokenBody, StandardCharsets.UTF_8)) .build(); - log.info("IAM统一身份认证换取Token请求,method={}, headers={}, body={}", httpRequest.method(), httpRequest.headers().map(), tokenBody); + log.info("IAM统一身份认证换取Token请求,url={}, method={}", properties.getTokenUrl(), httpRequest.method()); HttpResponse response = httpClient.send( httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8) @@ -292,12 +295,12 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { .header(HttpHeaders.AUTHORIZATION, profileAuthorizationHeader(accessToken)) .GET() .build(); - log.info("IAM统一身份认证获取用户信息请求,headers={}, body={}", httpRequest.headers().map(), "无"); + log.info("IAM统一身份认证获取用户信息请求,url={}, method={}", properties.getProfileUrl(), httpRequest.method()); HttpResponse response = httpClient.send( httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8) ); - log.info("IAM统一身份认证获取用户信息响应,headers={}, body={}", response.headers().map(), response.body()); + log.info("IAM统一身份认证获取用户信息响应,status={}", response.statusCode()); if (response.statusCode() < 200 || response.statusCode() >= 300) { log.warn("IAM统一身份认证获取用户信息失败,status={}", response.statusCode()); throw new UserInvalidException(OAuth2TokenConstant.TOKEN_NOT_CORRECT); @@ -321,10 +324,24 @@ public class IamSsoTokenGranter extends AuthorizationCodeGranter { if (StringUtil.isNotBlank(request.getState())) { params.put("state", request.getState()); } - params.put("redirect_uri", properties.getRedirectUri()); + params.put("redirect_uri", resolveRedirectUri(request)); return params; } + /** + * 获取 IAM 回调地址,优先使用前端请求传入的值,兼容未传参时的后端默认配置。 + * + * @param request OAuth2 请求 + * @return IAM 回调地址 + */ + private String resolveRedirectUri(OAuth2Request request) { + String requestRedirectUri = normalizeRedirectUri(request.getRedirectUri()); + if (StringUtil.isNotBlank(requestRedirectUri)) { + return requestRedirectUri; + } + return normalizeRedirectUri(properties.getRedirectUri()); + } + private String buildTokenBody(Map params) { return params.entrySet().stream() .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue())) diff --git a/blade-auth/src/main/resources/application.yml b/blade-auth/src/main/resources/application.yml index ae6f2c9..586debc 100644 --- a/blade-auth/src/main/resources/application.yml +++ b/blade-auth/src/main/resources/application.yml @@ -89,3 +89,16 @@ social: client-id: 233************ client-secret: 233************************************ redirect-uri: ${social.domain}/oauth/redirect/dingtalk + +# IAM统一身份认证 +iam: + sso: + token-url: ${IAM_SSO_TOKEN_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_SSO_TOKEN} + profile-url: ${IAM_SSO_PROFILE_URL:http://172.16.204.83:38000/gwzh/IAM/IAM_SSO_PROFILE} + client-id: ${IAM_SSO_CLIENT_ID:f0b52f23f71b1649c468} + client-secret: ${IAM_SSO_CLIENT_SECRET:5b3d8575f0899946623ab86523ac3dd8ee98} + system-client-id: ${IAM_SSO_SYSTEM_CLIENT_ID:saber3} + system-client-secret: ${IAM_SSO_SYSTEM_CLIENT_SECRET:saber3_secret} + redirect-uri: ${IAM_SSO_REDIRECT_URI:http://172.16.203.228:8000/callback} + authorization: ${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} + profile-authorization: ${IAM_SSO_PROFILE_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=} diff --git a/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java b/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java index 9520b05..86787cf 100644 --- a/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java +++ b/blade-gateway/src/main/java/org/springblade/gateway/provider/AuthProvider.java @@ -61,6 +61,7 @@ public class AuthProvider { DEFAULT_SKIP_URL.add("/process/diagram-view"); DEFAULT_SKIP_URL.add("/manager/check-upload"); DEFAULT_SKIP_URL.add("/assets/**"); + DEFAULT_SKIP_URL.add("/iam/sso/token/**"); } /** diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java index 97020dd..a042452 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java @@ -70,6 +70,7 @@ public interface ISysClient { String PARAM = API_PREFIX + "/param"; String PARAM_VALUE = API_PREFIX + "/param-value"; String REGION = API_PREFIX + "/region"; + String FEE_ITEMS = API_PREFIX + "/fee-items"; /** * 获取菜单 @@ -292,4 +293,12 @@ public interface ISysClient { @GetMapping(REGION) R getRegion(@RequestParam("code") String code); + /** + * 获取启用的费用项 + * + * @return 费用项集合 + */ + @GetMapping(FEE_ITEMS) + R> getFeeItems(); + } diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java index 47e40d7..fe7d1d0 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java @@ -159,5 +159,10 @@ public class ISysClientFallback implements ISysClient { return R.fail("获取数据失败"); } + @Override + public R> getFeeItems() { + return R.fail("获取数据失败"); + } + } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java new file mode 100644 index 0000000..b8d873d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementPaymentRequest.java @@ -0,0 +1,23 @@ +/** + * 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.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** 正式结算付款申请请求。 @author Chill */ +@Data +public class FormalSettlementPaymentRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private BigDecimal appliedAmount; + private String remark; +} 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 new file mode 100644 index 0000000..5bb3c82 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementSaveRequest.java @@ -0,0 +1,31 @@ +/** + * 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.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** 正式结算保存请求。 @author Chill */ +@Data +public class FormalSettlementSaveRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private Long contractId; + private String settlementType; + private List sourcePreSettlementIds; + private List sourceDetailIds; + private LocalDate exchangeRateDate; + private BigDecimal exchangeRate; + private String attachmentsJson; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java new file mode 100644 index 0000000..e48fac7 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/FormalSettlementStatusRequest.java @@ -0,0 +1,21 @@ +/** + * 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.dto; + +import lombok.Data; +import java.io.Serial; +import java.io.Serializable; + +/** 正式结算状态操作请求。 @author Chill */ +@Data +public class FormalSettlementStatusRequest implements Serializable { + @Serial private static final long serialVersionUID = 1L; + private Long id; + private String reason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java new file mode 100644 index 0000000..9827a15 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementAdvanceRequest.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +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 PreSettlementAdvanceRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "申请预付金额") + private BigDecimal appliedAmount; + + @Schema(description = "金蝶预付单号") + private String kingdeeAdvanceNo; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java new file mode 100644 index 0000000..bbfddec --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementDetailAdjustRequest.java @@ -0,0 +1,77 @@ +/** + * 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.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 预结算明细调整请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算明细调整请求") +public class PreSettlementDetailAdjustRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算明细ID") + private Long detailId; + + @Schema(description = "调整原因") + private String changeReason; + + @Schema(description = "明细费用行") + private List rows; + + @Data + @Schema(description = "明细费用行") + public static class FeeRow implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "费用快照ID") + private Long id; + + @Schema(description = "运输总量") + private BigDecimal transportQuantity; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "运输单价") + private BigDecimal unitPrice; + + @Schema(description = "运费") + private BigDecimal freightAmount; + + @Schema(description = "费用项目") + private Map feeItems; + + @Schema(description = "结算金额(含税)") + private BigDecimal settlementAmountTax; + + @Schema(description = "结算金额(不含税)") + private BigDecimal settlementAmountNoTax; + + @Schema(description = "备注") + private String remark; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java new file mode 100644 index 0000000..1140702 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementSaveRequest.java @@ -0,0 +1,99 @@ +/** + * 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 io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * 预结算单保存请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算单保存请求") +public class PreSettlementSaveRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long id; + + @Schema(description = "合同ID") + private Long contractId; + + @Schema(description = "汇率日期") + private LocalDate exchangeRateDate; + + @Schema(description = "结算汇率") + private BigDecimal exchangeRate; + + @Schema(description = "附件JSON") + private String attachmentsJson; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "应收应付明细ID") + private List sourceDetailIds; + + @Schema(description = "结算合计费用") + private List summaryFees; + + @Data + @Schema(description = "结算合计费用") + public static class SummaryFee implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "合计费用ID") + private Long id; + + @Schema(description = "费用类型") + private String feeType; + + @Schema(description = "费用项") + private String feeItem; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否手工添加") + private Integer manualFlag; + } + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.java new file mode 100644 index 0000000..3d30ff6 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/PreSettlementStatusRequest.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.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 预结算状态操作请求 + * + * @author Chill + */ +@Data +@Schema(description = "预结算状态操作请求") +public class PreSettlementStatusRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long id; + + @Schema(description = "操作原因") + private String reason; + + @Schema(description = "当前节点") + private String currentNode; + + @Schema(description = "当前处理人") + private String currentProcessor; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java new file mode 100644 index 0000000..29e9a11 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 正式结算单实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement") +@Schema(description = "正式结算单") +public class FormalSettlement extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private String formalSettlementNo; + private String sourceType; + private String settlementType; + 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 String localCurrency; + private BigDecimal localSettlementAmount; + private BigDecimal appliedPaymentAmount; + private BigDecimal paidAmount; + private LocalDate exchangeRateDate; + private BigDecimal exchangeRate; + private String invoiceStatus; + private String paymentStatus; + private String approvalStatus; + private String currentNode; + private String currentProcessor; + private String kingdeeBillNo; + private String kingdeeSyncStatus; + private String attachmentsJson; + private String remark; + private LocalDateTime approvedTime; + private LocalDateTime syncedTime; + private String voidReason; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java new file mode 100644 index 0000000..4aedce4 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetail.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 正式结算明细快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_detail") +public class FormalSettlementDetail extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Long sourcePreSettlementId; + private Long sourcePreSettlementDetailId; + private Long sourceDetailId; + private Integer lineNo; + private String documentNo; + private Long waybillId; + 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 BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private String batchNo; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal originalAmount; + private BigDecimal adjustAmount; + private BigDecimal settlementAmountTax; + private BigDecimal settlementAmountNoTax; + private String currency; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java new file mode 100644 index 0000000..b7e5892 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementDetailFee.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算货物费用快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_detail_fee") +public class FormalSettlementDetailFee extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementDetailId; + private Long sourceFeeId; + private String lineNo; + private String cargoName; + private String cargoType; + private BigDecimal transportQuantity; + private String quantityUnit; + private BigDecimal mileage; + private BigDecimal unitPrice; + private BigDecimal freightAmount; + private String feeItemsJson; + private BigDecimal originalAmount; + private BigDecimal adjustAmount; + private BigDecimal settlementAmountTax; + private BigDecimal settlementAmountNoTax; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java new file mode 100644 index 0000000..962f7ad --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementPayment.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算付款申请实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_payment") +public class FormalSettlementPayment extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private String paymentNo; + private String paymentType; + private BigDecimal appliedAmount; + private BigDecimal paidAmount; + private String billStatus; + private String kingdeeBillNo; + private String remark; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java new file mode 100644 index 0000000..1f0569b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlementSource.java @@ -0,0 +1,35 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 正式结算来源预结算实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_formal_settlement_source") +public class FormalSettlementSource extends TenantEntity { + @Serial private static final long serialVersionUID = 1L; + private Long formalSettlementId; + private Long preSettlementId; + private String preSettlementNo; + private BigDecimal settlementAmount; + private BigDecimal advanceAppliedAmount; + private BigDecimal advancePaidAmount; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java new file mode 100644 index 0000000..7181170 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlement.java @@ -0,0 +1,143 @@ +/** + * 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.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_pre_settlement") +@Schema(description = "预结算单") +public class PreSettlement extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单号") + private String preSettlementNo; + + @Schema(description = "来源") + private String sourceType; + + @Schema(description = "结算类型:receivable/payable") + private String settlementType; + + @Schema(description = "项目ID") + private Long projectId; + + @Schema(description = "项目名称") + private String projectName; + + @Schema(description = "所属组织ID") + private Long deptId; + + @Schema(description = "所属组织") + private String deptName; + + @Schema(description = "合同ID") + private Long contractId; + + @Schema(description = "合同编号") + private String contractNo; + + @Schema(description = "合同名称") + private String contractName; + + @Schema(description = "付款方") + private String payerName; + + @Schema(description = "收款方") + private String payeeName; + + @Schema(description = "结算币种") + private String currency; + + @Schema(description = "结算金额") + private BigDecimal settlementAmount; + + @Schema(description = "本位币") + private String localCurrency; + + @Schema(description = "本位币合计") + private BigDecimal localSettlementAmount; + + @Schema(description = "汇率日期") + private LocalDate exchangeRateDate; + + @Schema(description = "结算汇率") + private BigDecimal exchangeRate; + + @Schema(description = "审核状态:draft/reviewing/approved/returned/voided") + private String approvalStatus; + + @Schema(description = "当前节点") + private String currentNode; + + @Schema(description = "当前处理人") + private String currentProcessor; + + @Schema(description = "预付单号") + private String advanceNo; + + @Schema(description = "申请预付金额") + private BigDecimal advanceAppliedAmount; + + @Schema(description = "已付款金额") + private BigDecimal advancePaidAmount; + + @Schema(description = "正式结算单号") + private String formalSettlementNo; + + @Schema(description = "附件JSON") + private String attachmentsJson; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "审核通过时间") + private LocalDateTime approvedTime; + + @Schema(description = "正式结算时间") + private LocalDateTime formalSettledTime; + + @Schema(description = "作废原因") + private String voidReason; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java new file mode 100644 index 0000000..45e2e17 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementAdvance.java @@ -0,0 +1,58 @@ +/** + * 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.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 预结算预付记录实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_advance") +@Schema(description = "预结算预付记录") +public class PreSettlementAdvance extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "预付单号") + private String advanceNo; + + @Schema(description = "申请预付金额") + private BigDecimal appliedAmount; + + @Schema(description = "已付款金额") + private BigDecimal paidAmount; + + @Schema(description = "单据状态:reviewing/approved/paid/returned/voided") + private String billStatus; + + @Schema(description = "金蝶预付单号") + private String kingdeeAdvanceNo; + + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java new file mode 100644 index 0000000..d1e86ba --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementChangeRecord.java @@ -0,0 +1,59 @@ +/** + * 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.time.LocalDateTime; + +/** + * 预结算变更记录实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_change_record") +@Schema(description = "预结算变更记录") +public class PreSettlementChangeRecord extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "变更类型") + private String changeType; + + @Schema(description = "行号") + private Integer lineNo; + + @Schema(description = "操作类型") + private String operationType; + + @Schema(description = "变更内容") + private String changeContent; + + @Schema(description = "操作人") + private String operatorName; + + @Schema(description = "变更原因") + private String changeReason; + + @Schema(description = "变更时间") + private LocalDateTime changeTime; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java new file mode 100644 index 0000000..e833aaf --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetail.java @@ -0,0 +1,117 @@ +/** + * 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.LocalDateTime; + +/** + * 预结算明细实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_detail") +@Schema(description = "预结算明细") +public class PreSettlementDetail extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "应收应付明细ID") + private Long sourceDetailId; + + @Schema(description = "行号") + private Integer lineNo; + + @Schema(description = "单据号") + private String documentNo; + + @Schema(description = "运单ID") + private Long waybillId; + + @Schema(description = "运单号") + private String waybillNo; + + @Schema(description = "车号") + private String vehicleNo; + + @Schema(description = "发货地址") + private String departureAddress; + + @Schema(description = "到货地址") + private String arrivalAddress; + + @Schema(description = "实际发货时间") + private LocalDateTime actualDepartureTime; + + @Schema(description = "实际完成时间") + private LocalDateTime actualCompletionTime; + + @Schema(description = "运输类型") + private String transportType; + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "货物类型") + private String cargoType; + + @Schema(description = "运输总量") + private BigDecimal transportQuantity; + + @Schema(description = "数量单位") + private String quantityUnit; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "批次号") + private String batchNo; + + @Schema(description = "运输单价") + private BigDecimal unitPrice; + + @Schema(description = "运费") + private BigDecimal freightAmount; + + @Schema(description = "费用项JSON") + private String feeItemsJson; + + @Schema(description = "原金额") + private BigDecimal originalAmount; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "结算金额(含税)") + private BigDecimal settlementAmountTax; + + @Schema(description = "结算金额(不含税)") + private BigDecimal settlementAmountNoTax; + + @Schema(description = "币种") + private String currency; + + @Schema(description = "备注") + private String remark; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java new file mode 100644 index 0000000..3d9dc3b --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementDetailFee.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 预结算明细费用快照实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_detail_fee") +@Schema(description = "预结算明细费用快照") +public class PreSettlementDetailFee extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算明细ID") + private Long preSettlementDetailId; + + @Schema(description = "源费用行ID") + private Long sourceFeeId; + + @Schema(description = "行号") + private String lineNo; + + @Schema(description = "货物名称") + private String cargoName; + + @Schema(description = "货物类型") + private String cargoType; + + @Schema(description = "运输量") + private BigDecimal transportQuantity; + + @Schema(description = "数量单位") + private String quantityUnit; + + @Schema(description = "里程") + private BigDecimal mileage; + + @Schema(description = "运输单价") + private BigDecimal unitPrice; + + @Schema(description = "运费") + private BigDecimal freightAmount; + + @Schema(description = "费用项JSON") + private String feeItemsJson; + + @Schema(description = "原金额") + private BigDecimal originalAmount; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "结算金额(含税)") + private BigDecimal settlementAmountTax; + + @Schema(description = "结算金额(不含税)") + private BigDecimal settlementAmountNoTax; + + @Schema(description = "备注") + private String remark; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java new file mode 100644 index 0000000..aa64ad3 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PreSettlementSummaryFee.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; +import java.math.BigDecimal; + +/** + * 预结算合计费用实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_pre_settlement_summary_fee") +@Schema(description = "预结算合计费用") +public class PreSettlementSummaryFee extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "预结算单ID") + private Long preSettlementId; + + @Schema(description = "行号") + private Integer lineNo; + + @Schema(description = "费用类型") + private String feeType; + + @Schema(description = "费用项") + private String feeItem; + + @Schema(description = "原金额") + private BigDecimal originalAmount; + + @Schema(description = "调整金额") + private BigDecimal adjustAmount; + + @Schema(description = "结算金额") + private BigDecimal settlementAmount; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "是否手工添加") + private Integer manualFlag; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java new file mode 100644 index 0000000..8dcd0fd --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -0,0 +1,43 @@ +/** + * 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 com.baomidou.mybatisplus.annotation.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.time.LocalDate; +import java.util.List; + +/** + * 正式结算单视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class FormalSettlementVO extends FormalSettlement { + @Serial private static final long serialVersionUID = 1L; + @TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createStartDate; + @TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createEndDate; + @TableField(exist = false) private String createUserName; + @TableField(exist = false) private String approvalStatusName; + @TableField(exist = false) private String settlementTypeName; + @TableField(exist = false) private String preSettlementNos; + @TableField(exist = false) private String preSettlementNo; + @TableField(exist = false) private List sources; + @TableField(exist = false) private List details; + @TableField(exist = false) private List payments; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java new file mode 100644 index 0000000..51a4ccd --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PreSettlementVO.java @@ -0,0 +1,108 @@ +/** + * 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.vo; + +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementAdvance; +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.springframework.format.annotation.DateTimeFormat; + +import java.io.Serial; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +/** + * 预结算单视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "预结算单") +public class PreSettlementVO extends PreSettlement { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField(exist = false) + @Schema(description = "生成开始日期") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate createStartDate; + + @TableField(exist = false) + @Schema(description = "生成结束日期") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate createEndDate; + + @TableField(exist = false) + @Schema(description = "创建人姓名") + private String createUserName; + + @TableField(exist = false) + @Schema(description = "更新人姓名") + private String updateUserName; + + @TableField(exist = false) + @Schema(description = "审核状态名称") + private String approvalStatusName; + + @TableField(exist = false) + @Schema(description = "结算类型名称") + private String settlementTypeName; + + @TableField(exist = false) + @Schema(description = "结算明细") + private List details; + + @TableField(exist = false) + @Schema(description = "明细费用快照") + private Map> detailFees; + + @TableField(exist = false) + @Schema(description = "结算合计") + private List summaryFees; + + @TableField(exist = false) + @Schema(description = "预付信息") + private List advances; + + @TableField(exist = false) + @Schema(description = "变更记录") + private List changeRecords; + + @TableField(exist = false) + @Schema(description = "打印模板") + private List> printTemplates; + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java index 3db4bef..9dce693 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IUserClient.java @@ -57,6 +57,7 @@ public interface IUserClient { String USER_BY_ACCOUNT = API_PREFIX + "/user-by-account"; String USER_AUTH_INFO = API_PREFIX + "/user-auth-info"; String SAVE_USER = API_PREFIX + "/save-user"; + String SAVE_IAM_USER = API_PREFIX + "/save-iam-user"; String REGISTER_USER = API_PREFIX + "/register-user"; String REMOVE_USER = API_PREFIX + "/remove-user"; @@ -149,6 +150,15 @@ public interface IUserClient { @PostMapping(SAVE_USER) R saveUser(@RequestBody User user); + /** + * 新建IAM用户 + * + * @param user 用户实体 + * @return 是否成功 + */ + @PostMapping(SAVE_IAM_USER) + R saveIamUser(@RequestBody User user); + /** * 注册用户 * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java index 0c12f90..82d63be 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java @@ -25,6 +25,7 @@ */ package org.springblade.system.feign; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.AllArgsConstructor; import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; @@ -63,6 +64,8 @@ public class SysClient implements ISysClient { private final IRegionService regionService; + private final IFeeItemService feeItemService; + @Override @GetMapping(MENU) public R

getMenu(Long id) { @@ -200,5 +203,14 @@ public class SysClient implements ISysClient { return R.data(regionService.getById(code)); } + @Override + @GetMapping(FEE_ITEMS) + public R> getFeeItems() { + return R.data(feeItemService.list(Wrappers.lambdaQuery() + .eq(FeeItem::getStatus, 1) + .eq(FeeItem::getIsDeleted, 0) + .orderByAsc(FeeItem::getFeeCategory, FeeItem::getName))); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java index ba1ccd4..df55138 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/UserClient.java @@ -108,6 +108,12 @@ public class UserClient implements IUserClient { return R.data(service.submit(user)); } + @Override + @PostMapping(SAVE_IAM_USER) + public R saveIamUser(@RequestBody User user) { + return R.data(service.saveIamUser(user)); + } + @Override @PostMapping(REGISTER_USER) public R registerUser(User user) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java index accb55f..e1ff5ee 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IUserService.java @@ -280,6 +280,14 @@ public interface IUserService extends BaseService { */ boolean registerUser(User user); + /** + * 新建IAM统一身份认证用户(按可信租户落库) + * + * @param user 用户实体 + * @return 是否成功 + */ + boolean saveIamUser(User user); + /** * 配置用户平台扩展信息(租户守卫校验用户归属) * diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java index 83ecc2c..944d99e 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/UserServiceImpl.java @@ -74,6 +74,7 @@ import org.springframework.transaction.annotation.Transactional; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -549,6 +550,35 @@ public class UserServiceImpl extends BaseServiceImpl implement return saveUser(user); } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean saveIamUser(User user) { + if (AuthUtil.hasAuth()) { + throw new ServiceException("IAM用户创建仅允许统一身份认证流程调用!"); + } + Tenant tenant = SysCache.getTenant(user.getTenantId()); + if (tenant == null || tenant.getId() == null) { + throw new ServiceException("租户信息错误!"); + } + if (user.getUserType() == null) { + user.setUserType(UserType.WEB.getCategory()); + } + if (StringUtil.isBlank(user.getRoleId())) { + user.setRoleId(StringPool.MINUS_ONE); + } + if (StringUtil.isBlank(user.getDeptId())) { + user.setDeptId(StringPool.MINUS_ONE); + } + if (StringUtil.isBlank(user.getPostId())) { + user.setPostId(StringPool.MINUS_ONE); + } + user.setIsOa(1); + user.setSyncTime(new Date()); + user.setStatus(StatusType.ACTIVE.getType()); + applyUserDefaults(user); + return saveUser(user); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean updatePlatform(Long userId, Integer userType, String userExt) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java new file mode 100644 index 0000000..85b1a0f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/FormalSettlementController.java @@ -0,0 +1,146 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.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.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.FormalSettlementDetailFee; +import org.springblade.transport.pojo.vo.FormalSettlementVO; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IFormalSettlementService; +import org.springblade.transport.service.IPreSettlementService; +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 java.util.List; +import java.util.Map; + +/** + * 正式结算单控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "formal_settlement") +@RequestMapping("/formal-settlement") +@Tag(name = "正式结算单", description = "正式结算单管理") +public class FormalSettlementController extends BladeController { + private final IFormalSettlementService formalSettlementService; + private final IPreSettlementService preSettlementService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "正式结算单分页") + public R> list(FormalSettlementVO query, Query pageQuery) { + return R.data(formalSettlementService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "正式结算单详情") + public R detail(@RequestParam Long id) { return R.data(formalSettlementService.detail(id)); } + + @GetMapping("/candidate-pre-settlements") + @ApiOperationSupport(order = 3) + @Operation(summary = "可合并的预结算单") + public R> candidates(PreSettlementVO query, Query pageQuery) { + return R.data(formalSettlementService.candidatePreSettlements(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/contract-options") + @ApiOperationSupport(order = 4) + @Operation(summary = "可选合同") + public R>> contractOptions(@RequestParam(required = false) String keyword) { + return R.data(preSettlementService.contractOptions(keyword)); + } + + @GetMapping("/candidate-details") + @ApiOperationSupport(order = 5) + @Operation(summary = "可选应收应付明细") + public R>> candidateDetails(Query query, @RequestParam Long contractId, + @RequestParam String settlementType, @RequestParam(required = false) String batchNo, + @RequestParam(required = false) String feeStartDate, @RequestParam(required = false) String feeEndDate) { + return R.data(preSettlementService.candidateDetails(Condition.getPage(query), contractId, + settlementType, batchNo, feeStartDate, feeEndDate)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 6) + @Operation(summary = "保存正式结算草稿") + public R save(@RequestBody FormalSettlementSaveRequest request) { return R.data(formalSettlementService.saveDraft(request)); } + + @PostMapping("/remove") + @ApiOperationSupport(order = 7) + @Operation(summary = "删除正式结算草稿") + public R remove(@RequestParam Long id) { formalSettlementService.removeDraft(id); return R.success("删除成功"); } + + @PostMapping("/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交审批") + public R submit(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.submit(request); return R.success("提交成功"); } + + @PostMapping("/approve") + @ApiOperationSupport(order = 9) + @Operation(summary = "审批通过") + public R approve(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.approve(request); return R.success("审批通过"); } + + @PostMapping("/return") + @ApiOperationSupport(order = 10) + @Operation(summary = "审批驳回") + public R returnBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.returnBill(request); return R.success("已驳回"); } + + @PostMapping("/void") + @ApiOperationSupport(order = 11) + @Operation(summary = "作废正式结算单") + public R voidBill(@RequestBody FormalSettlementStatusRequest request) { formalSettlementService.voidBill(request); return R.success("作废成功"); } + + @PostMapping("/sync-kingdee") + @ApiOperationSupport(order = 12) + @Operation(summary = "推送金蝶应付单") + public R syncKingdee(@RequestParam Long id) { return R.data(formalSettlementService.syncKingdee(id)); } + + @GetMapping("/detail-fees") + @ApiOperationSupport(order = 13) + @Operation(summary = "正式结算货物费用快照") + public R> detailFees(@RequestParam Long detailId) { + return R.data(formalSettlementService.detailFees(detailId)); + } + + @PostMapping("/adjust-detail") + @ApiOperationSupport(order = 14) + @Operation(summary = "调整草稿结算明细") + public R adjustDetail(@RequestBody PreSettlementDetailAdjustRequest request) { + formalSettlementService.adjustDetail(request); + return R.success("保存成功"); + } + + @PostMapping("/apply-payment") + @ApiOperationSupport(order = 15) + @Operation(summary = "发起尾款付款申请") + public R applyPayment(@RequestBody FormalSettlementPaymentRequest request) { + return R.data(formalSettlementService.applyPayment(request)); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java new file mode 100644 index 0000000..09ec05a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/PreSettlementController.java @@ -0,0 +1,262 @@ +/** + * 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.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.excel.util.ExcelUtil; +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.transport.excel.PreSettlementExcel; +import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; +import org.springblade.transport.pojo.dto.PreSettlementStatusRequest; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IPreSettlementService; +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 java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Map; + +/** + * 预结算单控制器 + * + * @author Chill + */ +@RestController +@AllArgsConstructor +@PreAuth(menu = "pre_settlement") +@RequestMapping("/pre-settlement") +@Tag(name = "预结算单", description = "预结算单管理") +public class PreSettlementController extends BladeController { + + private final IPreSettlementService preSettlementService; + + @GetMapping("/list") + @ApiOperationSupport(order = 1) + @Operation(summary = "预结算单分页") + public R> list(PreSettlementVO query, Query pageQuery) { + return R.data(preSettlementService.selectPage(Condition.getPage(pageQuery), query)); + } + + @GetMapping("/detail") + @ApiOperationSupport(order = 2) + @Operation(summary = "预结算单详情") + public R detail(@RequestParam Long id) { + return R.data(preSettlementService.detail(id)); + } + + @GetMapping("/contract-options") + @ApiOperationSupport(order = 3) + @Operation(summary = "可选合同") + public R>> contractOptions(@RequestParam(required = false) String keyword) { + return R.data(preSettlementService.contractOptions(keyword)); + } + + @GetMapping("/fee-options") + @ApiOperationSupport(order = 4) + @Operation(summary = "费用类型及费用项") + public R>> feeOptions() { + return R.data(preSettlementService.feeOptions()); + } + + @GetMapping("/candidate-details") + @ApiOperationSupport(order = 5) + @Operation(summary = "可选应收应付明细") + public R>> candidateDetails(Query query, @RequestParam Long contractId, + @RequestParam String settlementType, @RequestParam(required = false) String batchNo, + @RequestParam(required = false) String feeStartDate, + @RequestParam(required = false) String feeEndDate) { + return R.data(preSettlementService.candidateDetails(Condition.getPage(query), contractId, + settlementType, batchNo, feeStartDate, feeEndDate)); + } + + @PostMapping("/save") + @ApiOperationSupport(order = 5) + @Operation(summary = "保存预结算草稿") + public R save(@RequestBody PreSettlementSaveRequest request) { + return R.data(preSettlementService.saveDraft(request)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 6) + @Operation(summary = "删除预结算草稿") + public R remove(@RequestParam Long id) { + preSettlementService.removeDraft(id); + return R.success("删除成功"); + } + + @PostMapping("/remove-detail") + @ApiOperationSupport(order = 7) + @Operation(summary = "移除预结算明细") + public R removeDetail(@RequestParam Long id, @RequestParam Long detailId) { + preSettlementService.removeDetail(id, detailId); + return R.success("移除成功"); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 8) + @Operation(summary = "提交预结算审批") + public R submit(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.submit(request); + return R.success("审批流程已发起"); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 9) + @Operation(summary = "预结算审批通过") + public R approve(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.approve(request); + return R.success("审批通过"); + } + + @PostMapping("/return") + @ApiOperationSupport(order = 10) + @Operation(summary = "预结算审批驳回") + public R returnBill(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.returnBill(request); + return R.success("已驳回"); + } + + @PostMapping("/void") + @ApiOperationSupport(order = 11) + @Operation(summary = "作废预结算单") + public R voidBill(@RequestBody PreSettlementStatusRequest request) { + preSettlementService.voidBill(request); + return R.success("作废成功"); + } + + @PostMapping("/apply-advance") + @ApiOperationSupport(order = 12) + @Operation(summary = "发起预付申请") + public R applyAdvance(@RequestBody PreSettlementAdvanceRequest request) { + preSettlementService.applyAdvance(request); + return R.success("预付申请提交成功"); + } + + @PostMapping("/update-advance-paid") + @ApiOperationSupport(order = 13) + @Operation(summary = "回写预付付款金额") + public R updateAdvancePaid(@RequestParam Long advanceId, @RequestParam BigDecimal paidAmount, + @RequestParam(required = false) String kingdeeAdvanceNo) { + preSettlementService.updateAdvancePaidAmount(advanceId, paidAmount, kingdeeAdvanceNo); + return R.success("付款金额更新成功"); + } + + @PostMapping("/void-advance") + @ApiOperationSupport(order = 14) + @Operation(summary = "作废预付申请") + public R voidAdvance(@RequestParam Long advanceId, @RequestParam(required = false) String reason) { + preSettlementService.voidAdvance(advanceId, reason); + return R.success("预付申请作废成功"); + } + + @PostMapping("/formal-settlement") + @ApiOperationSupport(order = 16) + @Operation(summary = "尾款结算") + public R formalSettlement(@RequestParam Long id) { + return R.data(preSettlementService.formalSettlement(id)); + } + + @GetMapping("/detail-fees") + @ApiOperationSupport(order = 15) + @Operation(summary = "结算明细费用") + public R> detailFees(@RequestParam Long detailId) { + return R.data(preSettlementService.detailFees(detailId)); + } + + @PostMapping("/adjust-detail") + @ApiOperationSupport(order = 17) + @Operation(summary = "调整结算明细") + public R adjustDetail(@RequestBody PreSettlementDetailAdjustRequest request) { + preSettlementService.adjustDetail(request); + return R.success("保存成功"); + } + + @GetMapping("/print-templates") + @ApiOperationSupport(order = 18) + @Operation(summary = "预结算打印模板") + public R>> printTemplates(@RequestParam Long id) { + return R.data(preSettlementService.printTemplates(id)); + } + + @GetMapping("/export") + @ApiOperationSupport(order = 19) + @Operation(summary = "导出预结算单") + public void export(PreSettlementVO query, HttpServletResponse response) { + IPage page = preSettlementService.selectPage(new Page<>(1, 100000), query); + List rows = page.getRecords().stream().map(this::toExcel).toList(); + ExcelUtil.export(response, "预结算单" + DateUtil.time(), "预结算单", rows, PreSettlementExcel.class); + } + + private PreSettlementExcel toExcel(PreSettlementVO vo) { + PreSettlementExcel excel = new PreSettlementExcel(); + excel.setPreSettlementNo(vo.getPreSettlementNo()); + excel.setSourceType(vo.getSourceType()); + excel.setPayerName(vo.getPayerName()); + excel.setPayeeName(vo.getPayeeName()); + excel.setProjectName(vo.getProjectName()); + excel.setDeptName(vo.getDeptName()); + excel.setContractNo(vo.getContractNo()); + excel.setContractName(vo.getContractName()); + excel.setSettlementAmount(formatMoney(vo.getSettlementAmount(), vo.getCurrency())); + excel.setLocalSettlementAmount(formatMoney(vo.getLocalSettlementAmount(), vo.getLocalCurrency())); + excel.setExchangeRate(vo.getExchangeRate() == null ? "" : vo.getExchangeRate().stripTrailingZeros().toPlainString()); + excel.setAdvanceAppliedAmount(formatMoney(vo.getAdvanceAppliedAmount(), vo.getCurrency())); + excel.setAdvancePaidAmount(formatMoney(vo.getAdvancePaidAmount(), vo.getCurrency())); + excel.setApprovalStatusName(vo.getApprovalStatusName()); + excel.setCurrentNode(vo.getCurrentNode()); + excel.setCurrentProcessor(vo.getCurrentProcessor()); + excel.setCreateUserName(vo.getCreateUserName()); + excel.setCreateTime(vo.getCreateTime()); + return excel; + } + + private String formatMoney(BigDecimal value, String currency) { + if (value == null) return ""; + return value.setScale(2, RoundingMode.HALF_UP).toPlainString() + " " + + (currency == null || currency.isBlank() ? "RMB" : currency); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java new file mode 100644 index 0000000..d6d15c0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/PreSettlementExcel.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import cn.idev.excel.annotation.write.style.ContentRowHeight; +import cn.idev.excel.annotation.write.style.HeadRowHeight; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 预结算单 Excel + * + * @author Chill + */ +@Data +@ColumnWidth(22) +@HeadRowHeight(20) +@ContentRowHeight(18) +public class PreSettlementExcel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ExcelProperty("预结算单号") + private String preSettlementNo; + @ExcelProperty("来源") + private String sourceType; + @ExcelProperty("付款方") + private String payerName; + @ExcelProperty("收款方") + private String payeeName; + @ExcelProperty("项目名称") + private String projectName; + @ExcelProperty("所属组织") + private String deptName; + @ExcelProperty("合同编号") + private String contractNo; + @ExcelProperty("合同名称") + private String contractName; + @ExcelProperty("原币结算金额") + private String settlementAmount; + @ExcelProperty("本位币结算金额") + private String localSettlementAmount; + @ExcelProperty("结算汇率") + private String exchangeRate; + @ExcelProperty("申请预付金额") + private String advanceAppliedAmount; + @ExcelProperty("已付款金额") + private String advancePaidAmount; + @ExcelProperty("审核状态") + private String approvalStatusName; + @ExcelProperty("当前节点") + private String currentNode; + @ExcelProperty("当前处理人") + private String currentProcessor; + @ExcelProperty("创建人") + private String createUserName; + @ExcelProperty("创建时间") + private Date createTime; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java new file mode 100644 index 0000000..a0d9ce7 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailFeeMapper.java @@ -0,0 +1,18 @@ +/** + * 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.FormalSettlementDetailFee; + +/** 正式结算货物费用 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementDetailFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java new file mode 100644 index 0000000..121c4af --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementDetailMapper.java @@ -0,0 +1,18 @@ +/** + * 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.FormalSettlementDetail; + +/** 正式结算明细 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementDetailMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java new file mode 100644 index 0000000..117950b --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementMapper.java @@ -0,0 +1,18 @@ +/** + * 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.FormalSettlement; + +/** 正式结算单 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java new file mode 100644 index 0000000..fe41e31 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementPaymentMapper.java @@ -0,0 +1,18 @@ +/** + * 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.FormalSettlementPayment; + +/** 正式结算付款申请 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementPaymentMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java new file mode 100644 index 0000000..793af53 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/FormalSettlementSourceMapper.java @@ -0,0 +1,18 @@ +/** + * 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.FormalSettlementSource; + +/** 正式结算来源 Mapper。 @author Chill */ +@Mapper +public interface FormalSettlementSourceMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.java new file mode 100644 index 0000000..c8bb155 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementAdvanceMapper.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.PreSettlementAdvance; + +/** + * 预结算预付记录 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementAdvanceMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.java new file mode 100644 index 0000000..0d96010 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementChangeRecordMapper.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.PreSettlementChangeRecord; + +/** + * 预结算变更记录 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementChangeRecordMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.java new file mode 100644 index 0000000..7c28dc4 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailFeeMapper.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.PreSettlementDetailFee; + +/** + * 预结算明细费用 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementDetailFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.java new file mode 100644 index 0000000..143eaaf --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementDetailMapper.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.PreSettlementDetail; + +/** + * 预结算明细 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementDetailMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.java new file mode 100644 index 0000000..c2b8648 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementMapper.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.PreSettlement; + +/** + * 预结算单 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.java new file mode 100644 index 0000000..c587af0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/PreSettlementSummaryFeeMapper.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.PreSettlementSummaryFee; + +/** + * 预结算合计费用 Mapper + * + * @author Chill + */ +@Mapper +public interface PreSettlementSummaryFeeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java new file mode 100644 index 0000000..531d798 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.FormalSettlementSaveRequest; +import org.springblade.transport.pojo.dto.FormalSettlementStatusRequest; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.vo.FormalSettlementVO; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import java.util.List; +import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest; + +/** + * 正式结算单服务 + * + * @author Chill + */ +public interface IFormalSettlementService extends BaseService { + IPage selectPage(IPage page, FormalSettlementVO query); + IPage candidatePreSettlements(IPage page, PreSettlementVO query); + FormalSettlementVO detail(Long id); + Long saveDraft(FormalSettlementSaveRequest request); + void removeDraft(Long id); + void submit(FormalSettlementStatusRequest request); + void approve(FormalSettlementStatusRequest request); + void returnBill(FormalSettlementStatusRequest request); + void voidBill(FormalSettlementStatusRequest request); + String syncKingdee(Long id); + List detailFees(Long detailId); + void adjustDetail(PreSettlementDetailAdjustRequest request); + String applyPayment(FormalSettlementPaymentRequest request); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java new file mode 100644 index 0000000..4133a41 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPreSettlementService.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; +import org.springblade.transport.pojo.dto.PreSettlementStatusRequest; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.vo.PreSettlementVO; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * 预结算单服务 + * + * @author Chill + */ +public interface IPreSettlementService extends BaseService { + + IPage selectPage(IPage page, PreSettlementVO query); + + PreSettlementVO detail(Long id); + + List> contractOptions(String keyword); + + List> feeOptions(); + + IPage> candidateDetails(IPage page, Long contractId, String settlementType, + String batchNo, String feeStartDate, String feeEndDate); + + Long saveDraft(PreSettlementSaveRequest request); + + void removeDraft(Long id); + + void removeDetail(Long id, Long detailId); + + void submit(PreSettlementStatusRequest request); + + void approve(PreSettlementStatusRequest request); + + void returnBill(PreSettlementStatusRequest request); + + void voidBill(PreSettlementStatusRequest request); + + void applyAdvance(PreSettlementAdvanceRequest request); + + void updateAdvancePaidAmount(Long advanceId, BigDecimal paidAmount, String kingdeeAdvanceNo); + + void voidAdvance(Long advanceId, String reason); + + String formalSettlement(Long id); + + List detailFees(Long detailId); + + void adjustDetail(PreSettlementDetailAdjustRequest request); + + List> printTemplates(Long id); + +} 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 new file mode 100644 index 0000000..4eb319f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java @@ -0,0 +1,511 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.service.impl; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +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.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.FormalSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementSourceMapper; +import org.springblade.transport.mapper.FormalSettlementPaymentMapper; +import org.springblade.transport.mapper.PreSettlementDetailMapper; +import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; +import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.ReceivablePayableDetailMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +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.FormalSettlementDetail; +import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; +import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.FormalSettlementPayment; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementDetail; +import org.springblade.transport.pojo.entity.PreSettlementDetailFee; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; +import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee; +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.service.IFormalSettlementService; +import org.springblade.transport.service.IContractManageService; +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.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.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 正式结算单服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class FormalSettlementServiceImpl extends BaseServiceImpl + implements IFormalSettlementService { + + 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 static final String VOIDED = "voided"; + private final FormalSettlementSourceMapper sourceMapper; + private final FormalSettlementPaymentMapper paymentMapper; + private final FormalSettlementDetailMapper detailMapper; + private final FormalSettlementDetailFeeMapper detailFeeMapper; + private final PreSettlementMapper preSettlementMapper; + private final PreSettlementDetailMapper preDetailMapper; + private final PreSettlementDetailFeeMapper preDetailFeeMapper; + private final ReceivablePayableDetailMapper receivablePayableMapper; + private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper; + private final IContractManageService contractManageService; + private final IWaybillService waybillService; + + @Override + public IPage selectPage(IPage page, FormalSettlementVO query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .like(Func.isNotEmpty(query.getFormalSettlementNo()), FormalSettlement::getFormalSettlementNo, query.getFormalSettlementNo()) + .like(Func.isNotEmpty(query.getProjectName()), FormalSettlement::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), FormalSettlement::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getContractNo()), FormalSettlement::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getContractName()), FormalSettlement::getContractName, query.getContractName()) + .like(Func.isNotEmpty(query.getPayerName()), FormalSettlement::getPayerName, query.getPayerName()) + .like(Func.isNotEmpty(query.getPayeeName()), FormalSettlement::getPayeeName, query.getPayeeName()) + .eq(Func.isNotEmpty(query.getSettlementType()), FormalSettlement::getSettlementType, query.getSettlementType()) + .eq(Func.isNotEmpty(query.getInvoiceStatus()), FormalSettlement::getInvoiceStatus, query.getInvoiceStatus()) + .eq(Func.isNotEmpty(query.getPaymentStatus()), FormalSettlement::getPaymentStatus, query.getPaymentStatus()) + .eq(Func.isNotEmpty(query.getKingdeeSyncStatus()), FormalSettlement::getKingdeeSyncStatus, query.getKingdeeSyncStatus()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), FormalSettlement::getApprovalStatus, query.getApprovalStatus()) + .ge(query.getCreateStartDate() != null, FormalSettlement::getCreateTime, query.getCreateStartDate() == null ? null : query.getCreateStartDate().atStartOfDay()) + .lt(query.getCreateEndDate() != null, FormalSettlement::getCreateTime, query.getCreateEndDate() == null ? null : query.getCreateEndDate().plusDays(1).atStartOfDay()); + if (Func.isNotEmpty(query.getPreSettlementNo())) { + List ids = sourceMapper.selectList(Wrappers.lambdaQuery() + .like(FormalSettlementSource::getPreSettlementNo, query.getPreSettlementNo())) + .stream().map(FormalSettlementSource::getFormalSettlementId).distinct().toList(); + if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids); + } + IPage result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); + return result.convert(this::toVO); + } + + @Override + public IPage candidatePreSettlements(IPage page, PreSettlementVO query) { + IPage result = preSettlementMapper.selectPage(page, Wrappers.lambdaQuery() + .eq(PreSettlement::getApprovalStatus, APPROVED) + .and(w -> w.isNull(PreSettlement::getFormalSettlementNo).or().eq(PreSettlement::getFormalSettlementNo, "")) + .eq(query.getContractId() != null, PreSettlement::getContractId, query.getContractId()) + .like(Func.isNotEmpty(query.getPreSettlementNo()), PreSettlement::getPreSettlementNo, query.getPreSettlementNo()) + .like(Func.isNotEmpty(query.getContractNo()), PreSettlement::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getContractName()), PreSettlement::getContractName, query.getContractName()) + .orderByDesc(PreSettlement::getCreateTime)); + return PreSettlementWrapper.build().pageVO(result); + } + + @Override + public FormalSettlementVO detail(Long id) { + FormalSettlement settlement = existing(id); + FormalSettlementVO vo = toVO(settlement); + vo.setSources(sourceMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementSource::getFormalSettlementId, id).orderByAsc(FormalSettlementSource::getCreateTime))); + vo.setDetails(detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo))); + vo.setPayments(paymentMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementPayment::getFormalSettlementId, id).orderByDesc(FormalSettlementPayment::getCreateTime))); + return vo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(FormalSettlementSaveRequest request) { + if (Func.isEmpty(request.getSourcePreSettlementIds()) && Func.isEmpty(request.getSourceDetailIds())) { + throw new ServiceException("请至少选择一张预结算单或一条应收应付明细"); + } + FormalSettlement settlement = request.getId() == null ? new FormalSettlement() : editable(request.getId()); + if (settlement.getId() != null) releaseSources(settlement); + List sources = Func.isEmpty(request.getSourcePreSettlementIds()) ? List.of() + : request.getSourcePreSettlementIds().stream().distinct().map(this::availableSource).toList(); + List directDetails = Func.isEmpty(request.getSourceDetailIds()) ? List.of() + : request.getSourceDetailIds().stream().distinct().map(this::availableDetail).toList(); + Long contractId = sources.isEmpty() ? request.getContractId() : sources.get(0).getContractId(); + String settlementType = sources.isEmpty() ? request.getSettlementType() : sources.get(0).getSettlementType(); + if (contractId == null || Func.isEmpty(settlementType)) throw new ServiceException("请选择合同并确认结算类型"); + if (sources.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()) + || !Objects.equals(settlementType, item.getSettlementType())) + || directDetails.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()) + || !Objects.equals(settlementType, item.getSettlementType()) + || (!sources.isEmpty() && !Objects.equals(sources.get(0).getCurrency(), item.getCurrency())))) { + throw new ServiceException("合并的预结算单必须属于同一合同、结算类型及币种"); + } + PreSettlement first = sources.isEmpty() ? null : sources.get(0); + ContractManage contract = contractManageService.getById(contractId); + if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("合同不存在"); + if (settlement.getId() == null) { + settlement.setFormalSettlementNo(nextNo()); + settlement.setApprovalStatus(DRAFT); + settlement.setCurrentNode("草稿"); + settlement.setSourceType(sources.isEmpty() ? "应收应付" : directDetails.isEmpty() ? "预结算合并" : "混合来源"); + settlement.setInvoiceStatus("unreceived"); + settlement.setPaymentStatus("unpaid"); + settlement.setKingdeeSyncStatus("unsynced"); + } + if (first == null) copyHeader(contract, settlementType, directDetails.get(0), settlement); else copyHeader(first, settlement); + settlement.setExchangeRateDate(request.getExchangeRateDate()); + settlement.setExchangeRate(request.getExchangeRate() == null ? BigDecimal.ONE : positive(request.getExchangeRate(), "结算汇率")); + BigDecimal sourceAmount = sources.stream().map(PreSettlement::getSettlementAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal detailAmount = directDetails.stream().map(ReceivablePayableDetail::getTotalAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + settlement.setSettlementAmount(sourceAmount.add(detailAmount)); + settlement.setAppliedPaymentAmount(sources.stream().map(PreSettlement::getAdvanceAppliedAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + settlement.setPaidAmount(sources.stream().map(PreSettlement::getAdvancePaidAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + settlement.setLocalSettlementAmount(settlement.getSettlementAmount().multiply(settlement.getExchangeRate())); + settlement.setAttachmentsJson(request.getAttachmentsJson()); + settlement.setRemark(limit(request.getRemark(), 200)); + saveOrUpdate(settlement); + rebuildSnapshots(settlement, sources, directDetails); + return settlement.getId(); + } + + @Override @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + FormalSettlement settlement = editable(id); + releaseSources(settlement); + sourceMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, id)); + List detailIds = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, id)).stream().map(FormalSettlementDetail::getId).toList(); + if (!detailIds.isEmpty()) detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)); + detailMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, id)); + removeById(id); + } + + @Override public void submit(FormalSettlementStatusRequest request) { changeStatus(request.getId(), DRAFT, REVIEWING, "财务审核", null); } + @Override public void returnBill(FormalSettlementStatusRequest request) { changeStatus(request.getId(), REVIEWING, RETURNED, "已驳回", request.getReason()); } + + @Override + public void approve(FormalSettlementStatusRequest request) { + FormalSettlement settlement = existing(request.getId()); + if (!REVIEWING.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批中的正式结算单允许审核"); + settlement.setApprovalStatus(APPROVED); + settlement.setCurrentNode("审批通过"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + settlement.setApprovedTime(LocalDateTime.now()); + updateById(settlement); + } + + @Override + public void voidBill(FormalSettlementStatusRequest request) { + FormalSettlement settlement = existing(request.getId()); + if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许作废"); + if ("synced".equals(settlement.getKingdeeSyncStatus())) throw new ServiceException("已同步金蝶的正式结算单不能直接作废"); + settlement.setApprovalStatus(VOIDED); + settlement.setCurrentNode("已作废"); + settlement.setVoidReason(required(limit(request.getReason(), 200), "作废原因")); + updateById(settlement); + } + + @Override + public String syncKingdee(Long id) { + FormalSettlement settlement = existing(id); + if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许同步金蝶"); + if ("synced".equals(settlement.getKingdeeSyncStatus())) return settlement.getKingdeeBillNo(); + String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()); + settlement.setKingdeeBillNo(kingdeeNo); + settlement.setKingdeeSyncStatus("synced"); + settlement.setSyncedTime(LocalDateTime.now()); + updateById(settlement); + return kingdeeNo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String applyPayment(FormalSettlementPaymentRequest request) { + FormalSettlement settlement = existing(request.getId()); + if (!APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许发起付款申请"); + if (!"payable".equals(settlement.getSettlementType())) throw new ServiceException("仅应付正式结算单允许发起付款申请"); + BigDecimal amount = positive(request.getAppliedAmount(), "申请付款金额"); + BigDecimal available = money(settlement.getSettlementAmount()).subtract(money(settlement.getAppliedPaymentAmount())); + if (amount.compareTo(available) > 0) throw new ServiceException("申请付款金额不能超过剩余可申请金额" + available); + FormalSettlementPayment payment = new FormalSettlementPayment(); + payment.setFormalSettlementId(settlement.getId()); + payment.setPaymentNo(nextPaymentNo()); + payment.setPaymentType("final"); + payment.setAppliedAmount(amount); + payment.setPaidAmount(BigDecimal.ZERO); + payment.setBillStatus(REVIEWING); + payment.setRemark(limit(request.getRemark(), 200)); + paymentMapper.insert(payment); + settlement.setAppliedPaymentAmount(money(settlement.getAppliedPaymentAmount()).add(amount)); + updateById(settlement); + return payment.getPaymentNo(); + } + + @Override + public List detailFees(Long detailId) { + FormalSettlementDetail detail = detailMapper.selectById(detailId); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("正式结算明细不存在"); + return detailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detailId) + .orderByAsc(FormalSettlementDetailFee::getLineNo)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void adjustDetail(PreSettlementDetailAdjustRequest request) { + FormalSettlementDetail detail = detailMapper.selectById(request.getDetailId()); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("正式结算明细不存在"); + FormalSettlement settlement = editable(detail.getFormalSettlementId()); + if (Func.isEmpty(request.getRows())) throw new ServiceException("请填写需要调整的货物费用行"); + String reason = required(limit(request.getChangeReason(), 200), "调整原因"); + Map existing = detailFees(detail.getId()).stream() + .collect(Collectors.toMap(FormalSettlementDetailFee::getId, item -> item)); + if (existing.size() != request.getRows().size()) throw new ServiceException("费用调整行数据不完整"); + for (PreSettlementDetailAdjustRequest.FeeRow row : request.getRows()) { + FormalSettlementDetailFee fee = existing.get(row.getId()); + if (fee == null) throw new ServiceException("存在无效的货物费用行"); + fee.setTransportQuantity(nonNegative(row.getTransportQuantity(), "运输总量")); + fee.setMileage(nonNegative(row.getMileage(), "里程")); + fee.setUnitPrice(nonNegative(row.getUnitPrice(), "运输单价")); + fee.setFreightAmount(nonNegative(row.getFreightAmount(), "运费")); + fee.setFeeItemsJson(JsonUtil.toJson(row.getFeeItems() == null ? java.util.Map.of() : row.getFeeItems())); + fee.setSettlementAmountTax(nonNegative(row.getSettlementAmountTax(), "结算金额(含税)")); + fee.setSettlementAmountNoTax(row.getSettlementAmountNoTax() == null ? null : nonNegative(row.getSettlementAmountNoTax(), "结算金额(不含税)")); + fee.setAdjustAmount(fee.getSettlementAmountTax().subtract(money(fee.getOriginalAmount()))); + fee.setRemark(limit(row.getRemark(), 200)); + detailFeeMapper.updateById(fee); + } + List rows = detailFees(detail.getId()); + detail.setTransportQuantity(rows.stream().map(FormalSettlementDetailFee::getTransportQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setFreightAmount(rows.stream().map(FormalSettlementDetailFee::getFreightAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setOriginalAmount(rows.stream().map(FormalSettlementDetailFee::getOriginalAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setSettlementAmountTax(rows.stream().map(FormalSettlementDetailFee::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setSettlementAmountNoTax(rows.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(detail.getOriginalAmount())); + detail.setRemark(reason); + detailMapper.updateById(detail); + BigDecimal amount = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream() + .map(FormalSettlementDetail::getSettlementAmountTax).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + settlement.setSettlementAmount(amount); + settlement.setLocalSettlementAmount(amount.multiply(settlement.getExchangeRate() == null ? BigDecimal.ONE : settlement.getExchangeRate())); + updateById(settlement); + } + + private void rebuildSnapshots(FormalSettlement settlement, List sources, List directDetails) { + List existingDetailIds = detailMapper.selectList(Wrappers.lambdaQuery() + .eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())).stream().map(FormalSettlementDetail::getId).toList(); + if (!existingDetailIds.isEmpty()) detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(FormalSettlementDetailFee::getFormalSettlementDetailId, existingDetailIds)); + sourceMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, settlement.getId())); + detailMapper.delete(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())); + int lineNo = 1; + for (PreSettlement source : sources) { + FormalSettlementSource relation = new FormalSettlementSource(); + relation.setFormalSettlementId(settlement.getId()); relation.setPreSettlementId(source.getId()); + relation.setPreSettlementNo(source.getPreSettlementNo()); relation.setSettlementAmount(source.getSettlementAmount()); + relation.setAdvanceAppliedAmount(source.getAdvanceAppliedAmount()); relation.setAdvancePaidAmount(source.getAdvancePaidAmount()); + sourceMapper.insert(relation); + int reserved = preSettlementMapper.update(null, Wrappers.lambdaUpdate() + .eq(PreSettlement::getId, source.getId()) + .eq(PreSettlement::getApprovalStatus, APPROVED) + .and(w -> w.isNull(PreSettlement::getFormalSettlementNo).or().eq(PreSettlement::getFormalSettlementNo, "")) + .set(PreSettlement::getFormalSettlementNo, settlement.getFormalSettlementNo()) + .set(PreSettlement::getFormalSettledTime, LocalDateTime.now()) + .set(PreSettlement::getCurrentNode, "已锁定(正式结算)")); + if (reserved != 1) throw new ServiceException("预结算单" + source.getPreSettlementNo() + "已被其他正式结算占用"); + for (PreSettlementDetail item : preDetailMapper.selectList(Wrappers.lambdaQuery().eq(PreSettlementDetail::getPreSettlementId, source.getId()))) { + FormalSettlementDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetail.class)); + detail.setId(null); detail.setFormalSettlementId(settlement.getId()); detail.setSourcePreSettlementId(source.getId()); + detail.setSourcePreSettlementDetailId(item.getId()); detail.setLineNo(lineNo++); detailMapper.insert(detail); + copyPreDetailFees(item, detail); + ReceivablePayableDetail original = receivablePayableMapper.selectById(item.getSourceDetailId()); + if (original != null) { original.setFormalSettlementNo(settlement.getFormalSettlementNo()); original.setSettlementStatus("formal_settled"); receivablePayableMapper.updateById(original); } + } + } + for (ReceivablePayableDetail source : directDetails) { + FormalSettlementDetail detail = new FormalSettlementDetail(); + detail.setFormalSettlementId(settlement.getId()); detail.setSourceDetailId(source.getId()); detail.setLineNo(lineNo++); + detail.setDocumentNo(source.getDocumentNo()); detail.setWaybillId(source.getWaybillId()); detail.setWaybillNo(source.getWaybillNo()); + detail.setVehicleNo(source.getVehicleNo()); detail.setTransportType(source.getTransportType()); detail.setCargoName(source.getCargoName()); + detail.setCargoType(source.getCargoType()); detail.setTransportQuantity(source.getTransportQuantity()); detail.setQuantityUnit(source.getQuantityUnit()); + detail.setMileage(source.getMileage()); detail.setBatchNo(source.getBatchNo()); detail.setUnitPrice(source.getUnitPrice()); + detail.setFreightAmount(money(source.getFreightAmount())); detail.setFeeItemsJson(source.getFeeItemsJson()); + detail.setOriginalAmount(money(source.getTotalAmount())); detail.setAdjustAmount(BigDecimal.ZERO); + detail.setSettlementAmountTax(money(source.getTotalAmount())); detail.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); + Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId()); + if (waybill != null) { + detail.setDepartureAddress(Func.isNotEmpty(waybill.getDepartureAddress()) ? waybill.getDepartureAddress() : waybill.getDepartureName()); + detail.setArrivalAddress(Func.isNotEmpty(waybill.getArrivalAddress()) ? waybill.getArrivalAddress() : waybill.getArrivalName()); + detail.setActualDepartureTime(waybill.getStartDate() == null ? null : waybill.getStartDate().atStartOfDay()); + detail.setActualCompletionTime(waybill.getEndDate() == null ? null : waybill.getEndDate().atStartOfDay()); + } + detail.setRemark(source.getRemark()); detailMapper.insert(detail); + copyDirectDetailFees(source, detail); + int affected = receivablePayableMapper.update(null, Wrappers.lambdaUpdate() + .eq(ReceivablePayableDetail::getId, source.getId()) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .and(w -> w.isNull(ReceivablePayableDetail::getPreSettlementNo).or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(w -> w.isNull(ReceivablePayableDetail::getFormalSettlementNo).or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")) + .set(ReceivablePayableDetail::getFormalSettlementNo, settlement.getFormalSettlementNo()) + .set(ReceivablePayableDetail::getSettlementStatus, "formal_settled")); + if (affected != 1) throw new ServiceException("单据" + source.getDocumentNo() + "已被其他结算单选择"); + } + } + + private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) { + List fees = preDetailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementDetailFee::getPreSettlementDetailId, source.getId()).orderByAsc(PreSettlementDetailFee::getLineNo)); + for (PreSettlementDetailFee item : fees) { + FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class)); + fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setSourceFeeId(item.getId()); detailFeeMapper.insert(fee); + } + if (fees.isEmpty()) createSingleFee(target); + } + + private void copyDirectDetailFees(ReceivablePayableDetail source, FormalSettlementDetail target) { + List fees = receivablePayableCargoFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, source.getId()).orderByAsc(ReceivablePayableCargoFee::getLineNo)); + for (ReceivablePayableCargoFee item : fees) { + FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(item, FormalSettlementDetailFee.class)); + fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setSourceFeeId(item.getId()); + fee.setSettlementAmountTax(item.getAfterAmount() == null ? money(item.getOriginalAmount()) : item.getAfterAmount()); + fee.setSettlementAmountNoTax(null); detailFeeMapper.insert(fee); + } + if (fees.isEmpty()) createSingleFee(target); + } + + private void createSingleFee(FormalSettlementDetail target) { + FormalSettlementDetailFee fee = Objects.requireNonNull(BeanUtil.copyProperties(target, FormalSettlementDetailFee.class)); + fee.setId(null); fee.setFormalSettlementDetailId(target.getId()); fee.setLineNo("0001"); detailFeeMapper.insert(fee); + } + + private void releaseSources(FormalSettlement settlement) { + for (FormalSettlementSource relation : sourceMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, settlement.getId()))) { + PreSettlement source = preSettlementMapper.selectById(relation.getPreSettlementId()); + if (source != null && Objects.equals(source.getFormalSettlementNo(), settlement.getFormalSettlementNo())) { + preSettlementMapper.update(null, Wrappers.lambdaUpdate() + .eq(PreSettlement::getId, source.getId()) + .set(PreSettlement::getFormalSettlementNo, null) + .set(PreSettlement::getFormalSettledTime, null) + .set(PreSettlement::getCurrentNode, "审批通过")); + } + } + for (FormalSettlementDetail detail : detailMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId()))) { + ReceivablePayableDetail source = receivablePayableMapper.selectById(detail.getSourceDetailId()); + if (source != null && Objects.equals(source.getFormalSettlementNo(), settlement.getFormalSettlementNo())) { + receivablePayableMapper.update(null, Wrappers.lambdaUpdate() + .eq(ReceivablePayableDetail::getId, source.getId()) + .set(ReceivablePayableDetail::getFormalSettlementNo, null) + .set(ReceivablePayableDetail::getSettlementStatus, + detail.getSourcePreSettlementId() == null ? "pending" : "pre_settled")); + } + } + } + + private FormalSettlementVO toVO(FormalSettlement entity) { + FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity); + vo.setPreSettlementNos(sourceMapper.selectList(Wrappers.lambdaQuery().eq(FormalSettlementSource::getFormalSettlementId, entity.getId())).stream().map(FormalSettlementSource::getPreSettlementNo).collect(Collectors.joining(","))); + return vo; + } + + private PreSettlement availableSource(Long id) { + PreSettlement source = preSettlementMapper.selectById(id); + if (source == null || Objects.equals(source.getIsDeleted(), 1)) throw new ServiceException("预结算单不存在"); + if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("仅审批通过的预结算单可生成正式结算"); + if (Func.isNotEmpty(source.getFormalSettlementNo())) throw new ServiceException("预结算单" + source.getPreSettlementNo() + "已被正式结算占用"); + return source; + } + + private ReceivablePayableDetail availableDetail(Long id) { + ReceivablePayableDetail detail = receivablePayableMapper.selectById(id); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("应收应付明细不存在"); + if (!"pending".equals(detail.getSettlementStatus()) || Func.isNotEmpty(detail.getPreSettlementNo()) || Func.isNotEmpty(detail.getFormalSettlementNo())) { + throw new ServiceException("单据" + detail.getDocumentNo() + "已被结算或关闭"); + } + return detail; + } + + private FormalSettlement existing(Long id) { + FormalSettlement entity = getById(id); + if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("正式结算单不存在"); + return entity; + } + + private FormalSettlement editable(Long id) { + FormalSettlement entity = existing(id); + if (!DRAFT.equals(entity.getApprovalStatus()) && !RETURNED.equals(entity.getApprovalStatus())) throw new ServiceException("仅草稿或已驳回的正式结算单允许编辑"); + return entity; + } + + private void changeStatus(Long id, String expected, String target, String node, String reason) { + FormalSettlement settlement = existing(id); + if (!expected.equals(settlement.getApprovalStatus()) && !(DRAFT.equals(expected) && RETURNED.equals(settlement.getApprovalStatus()))) throw new ServiceException("当前状态不允许该操作"); + settlement.setApprovalStatus(target); settlement.setCurrentNode(node); settlement.setCurrentProcessor(AuthUtil.getUserName()); + if (RETURNED.equals(target)) settlement.setVoidReason(limit(reason, 200)); + updateById(settlement); + } + + private void copyHeader(PreSettlement source, FormalSettlement target) { + target.setSettlementType(source.getSettlementType()); target.setProjectId(source.getProjectId()); target.setProjectName(source.getProjectName()); + target.setDeptId(source.getDeptId()); target.setDeptName(source.getDeptName()); target.setContractId(source.getContractId()); target.setContractNo(source.getContractNo()); + target.setContractName(source.getContractName()); target.setPayerName(source.getPayerName()); target.setPayeeName(source.getPayeeName()); + target.setCurrency(source.getCurrency()); target.setLocalCurrency(source.getLocalCurrency()); + } + + private void copyHeader(ContractManage contract, String settlementType, ReceivablePayableDetail source, FormalSettlement target) { + target.setSettlementType(settlementType); target.setProjectId(contract.getProjectId()); target.setProjectName(contract.getProjectName()); + target.setDeptId(contract.getOrganizationId()); target.setDeptName(contract.getOrganizationName()); target.setContractId(contract.getId()); + target.setContractNo(contract.getContractNo()); target.setContractName(contract.getContractName()); + if ("receivable".equals(settlementType)) { target.setPayerName(source.getCustomerName()); target.setPayeeName(contract.getPartyA()); } + else { target.setPayerName(contract.getPartyA()); target.setPayeeName(source.getCustomerName()); } + target.setCurrency(Func.isEmpty(source.getCurrency()) ? "RMB" : source.getCurrency()); target.setLocalCurrency("RMB"); + } + + private synchronized String nextNo() { + String prefix = "JS" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + long count = count(Wrappers.lambdaQuery().likeRight(FormalSettlement::getFormalSettlementNo, prefix)); + return prefix + String.format("%04d", count + 1); + } + + private synchronized String nextPaymentNo() { + String prefix = "FK" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + long count = paymentMapper.selectCount(Wrappers.lambdaQuery() + .likeRight(FormalSettlementPayment::getPaymentNo, prefix)); + return prefix + String.format("%04d", count + 1); + } + + private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.signum() < 0) throw new ServiceException(field + "不能小于0"); return value; } + private BigDecimal positive(BigDecimal value, String field) { if (value == null || value.signum() <= 0) throw new ServiceException(field + "必须大于0"); return value; } + private String required(String value, String field) { if (Func.isEmpty(value)) throw new ServiceException("请填写" + field); return value; } + private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("内容不能超过" + max + "个字"); return value; } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java new file mode 100644 index 0000000..e90abaf --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -0,0 +1,1404 @@ +/** + * 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.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 com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.mapper.PreSettlementAdvanceMapper; +import org.springblade.transport.mapper.PreSettlementChangeRecordMapper; +import org.springblade.transport.mapper.PreSettlementDetailFeeMapper; +import org.springblade.transport.mapper.PreSettlementDetailMapper; +import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; +import org.springblade.transport.mapper.ReceivablePayableDetailMapper; +import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest; +import org.springblade.transport.pojo.dto.PreSettlementDetailAdjustRequest; +import org.springblade.transport.pojo.dto.PreSettlementSaveRequest; +import org.springblade.transport.pojo.dto.PreSettlementStatusRequest; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.CustomerArchive; +import org.springblade.transport.pojo.entity.PreSettlement; +import org.springblade.transport.pojo.entity.PreSettlementAdvance; +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.ReceivablePayableCargoFee; +import org.springblade.transport.pojo.entity.ReceivablePayableDetail; +import org.springblade.transport.pojo.entity.Waybill; +import org.springblade.transport.pojo.vo.PreSettlementVO; +import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.service.ICustomerArchiveService; +import org.springblade.transport.service.IPreSettlementService; +import org.springblade.transport.service.IProjectApplyService; +import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.wrapper.PreSettlementWrapper; +import org.springblade.system.cache.UserCache; +import org.springblade.system.feign.ISysClient; +import org.springblade.system.pojo.entity.DictBiz; +import org.springblade.system.pojo.entity.FeeItem; +import org.springblade.system.cache.DictBizCache; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 预结算单服务实现类 + * + * @author Chill + */ +@Service +@RequiredArgsConstructor +public class PreSettlementServiceImpl extends BaseServiceImpl + implements IPreSettlementService { + + private static final String STATUS_DRAFT = "draft"; + private static final String STATUS_REVIEWING = "reviewing"; + private static final String STATUS_APPROVED = "approved"; + private static final String STATUS_RETURNED = "returned"; + private static final String STATUS_VOIDED = "voided"; + private static final String LOCAL_CURRENCY = "RMB"; + + private final PreSettlementDetailMapper detailMapper; + private final PreSettlementDetailFeeMapper detailFeeMapper; + private final PreSettlementSummaryFeeMapper summaryFeeMapper; + private final PreSettlementAdvanceMapper advanceMapper; + private final PreSettlementChangeRecordMapper changeRecordMapper; + private final ReceivablePayableDetailMapper sourceDetailMapper; + private final ReceivablePayableCargoFeeMapper sourceFeeMapper; + private final IContractManageService contractManageService; + private final IProjectApplyService projectApplyService; + private final ICustomerArchiveService customerArchiveService; + private final IWaybillService waybillService; + private final ISysClient sysClient; + + @Override + public IPage selectPage(IPage page, PreSettlementVO query) { + return PreSettlementWrapper.build().pageVO(page(page, buildQuery(query))); + } + + @Override + public List> feeOptions() { + Map categoryNames = DictBizCache.getList("fee_category").stream() + .collect(Collectors.toMap(DictBiz::getDictKey, DictBiz::getDictValue, (first, second) -> first, + LinkedHashMap::new)); + org.springblade.core.tool.api.R> response = sysClient.getFeeItems(); + if (response == null || !response.isSuccess() || response.getData() == null) { + throw new ServiceException("费用项基础档案读取失败,请稍后重试"); + } + return response.getData().stream() + .collect(Collectors.groupingBy(FeeItem::getFeeCategory, LinkedHashMap::new, Collectors.toList())) + .entrySet().stream().map(entry -> { + Map option = new LinkedHashMap<>(); + option.put("feeType", entry.getKey()); + option.put("feeTypeName", categoryNames.getOrDefault(entry.getKey(), entry.getKey())); + option.put("feeItems", entry.getValue().stream().map(FeeItem::getName).distinct().toList()); + return option; + }).toList(); + } + + @Override + public PreSettlementVO detail(Long id) { + PreSettlement settlement = loadExisting(id); + PreSettlementVO vo = PreSettlementWrapper.build().entityVO(settlement); + List details = listDetails(id); + vo.setDetails(details); + List detailIds = details.stream().map(PreSettlementDetail::getId).toList(); + Map> feeMap = Func.isEmpty(detailIds) ? new HashMap<>() + : detailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds) + .eq(PreSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(PreSettlementDetailFee::getCreateTime)) + .stream().collect(Collectors.groupingBy(PreSettlementDetailFee::getPreSettlementDetailId, + LinkedHashMap::new, Collectors.toList())); + vo.setDetailFees(feeMap); + vo.setSummaryFees(listSummaryFees(id)); + List advances = listAdvances(id); + advances.forEach(advance -> advance.setCreateUserName(UserCache.getUserRealName(advance.getCreateUser()))); + vo.setAdvances(advances); + vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementChangeRecord::getPreSettlementId, id) + .eq(PreSettlementChangeRecord::getIsDeleted, 0) + .orderByDesc(PreSettlementChangeRecord::getChangeTime))); + vo.setPrintTemplates(resolvePrintTemplates(settlement)); + return vo; + } + + @Override + public List> contractOptions(String keyword) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .in(ContractManage::getApprovalStatus, "approved", "change_approved") + .ne(ContractManage::getContractStage, "terminated") + .and(Func.isNotEmpty(keyword), query -> query.like(ContractManage::getContractName, keyword) + .or().like(ContractManage::getContractNo, keyword)) + .orderByDesc(ContractManage::getCreateTime); + return contractManageService.list(wrapper).stream().filter(this::isAvailableContract).map(contract -> { + Map result = new LinkedHashMap<>(); + result.put("id", contract.getId()); + result.put("contractNo", contract.getContractNo()); + result.put("contractName", contract.getContractName()); + result.put("projectId", contract.getProjectId()); + result.put("projectName", contract.getProjectName()); + result.put("deptId", contract.getOrganizationId()); + result.put("deptName", contract.getOrganizationName()); + result.put("partyA", contract.getPartyA()); + result.put("partyB", contract.getPartyB()); + result.put("settlementType", contractSettlementType(contract)); + return result; + }).toList(); + } + + @Override + public IPage> candidateDetails(IPage page, Long contractId, String settlementType, + String batchNo, String feeStartDate, String feeEndDate) { + if (contractId == null) { + throw new ServiceException("请先选择合同"); + } + validateSettlementType(settlementType); + ContractManage contract = loadAvailableContract(contractId); + if (!Objects.equals(contractSettlementType(contract), settlementType)) { + throw new ServiceException("结算类型与合同类别不一致"); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(ReceivablePayableDetail::getIsDeleted, 0) + .eq(ReceivablePayableDetail::getContractId, contractId) + .eq(ReceivablePayableDetail::getProjectId, contract.getProjectId()) + .eq(ReceivablePayableDetail::getDeptId, contract.getOrganizationId()) + .eq(ReceivablePayableDetail::getSettlementType, settlementType) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .and(query -> query.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, "")) + .and(query -> query.isNull(ReceivablePayableDetail::getFormalSettlementNo) + .or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")) + .and(query -> query.eq(ReceivablePayableDetail::getCustomerName, contract.getPartyA()) + .or().eq(ReceivablePayableDetail::getCustomerName, contract.getPartyB())) + .like(Func.isNotEmpty(batchNo), ReceivablePayableDetail::getBatchNo, batchNo) + .ge(Func.isNotEmpty(feeStartDate), ReceivablePayableDetail::getFeeDate, parseDate(feeStartDate)) + .le(Func.isNotEmpty(feeEndDate), ReceivablePayableDetail::getFeeDate, parseDate(feeEndDate)) + .orderByDesc(ReceivablePayableDetail::getCreateTime); + IPage sourcePage = sourceDetailMapper.selectPage( + new Page<>(page.getCurrent(), page.getSize()), wrapper); + Page> result = new Page<>(sourcePage.getCurrent(), sourcePage.getSize(), sourcePage.getTotal()); + result.setRecords(sourcePage.getRecords().stream().map(this::candidateMap).toList()); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long saveDraft(PreSettlementSaveRequest request) { + if (request.getContractId() == null) { + throw new ServiceException("请选择合同名称"); + } + if (request.getRemark() != null && request.getRemark().length() > 200) { + throw new ServiceException("备注不能超过200个字符"); + } + ContractManage contract = loadAvailableContract(request.getContractId()); + PreSettlement settlement = request.getId() == null ? new PreSettlement() : loadEditable(request.getId()); + boolean created = settlement.getId() == null; + List existingDetails = created ? List.of() : listDetails(settlement.getId()); + boolean contractChanged = !created && !Objects.equals(settlement.getContractId(), contract.getId()); + if (contractChanged && !existingDetails.isEmpty()) { + throw new ServiceException("预结算单已存在结算明细,不能更换合同"); + } + if (contractChanged) { + settlement.setSettlementType(contractSettlementType(contract)); + settlement.setCurrency(LOCAL_CURRENCY); + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlement.getId())); + } + if (created) { + settlement.setPreSettlementNo(nextPreSettlementNo()); + settlement.setSourceType("应收应付"); + settlement.setSettlementType(resolveSettlementType(request, contract)); + settlement.setApprovalStatus(STATUS_DRAFT); + settlement.setCurrentNode("草稿"); + settlement.setCurrency(LOCAL_CURRENCY); + settlement.setLocalCurrency(LOCAL_CURRENCY); + } + fillContract(settlement, contract); + settlement.setExchangeRateDate(request.getExchangeRateDate()); + settlement.setExchangeRate(request.getExchangeRate()); + settlement.setAttachmentsJson(request.getAttachmentsJson()); + settlement.setRemark(request.getRemark()); + saveOrUpdate(settlement); + if (request.getSourceDetailIds() != null) { + synchronizeDetails(settlement, request.getSourceDetailIds()); + } + settlement.setExchangeRate(normalizeRate(request.getExchangeRate(), settlement.getCurrency())); + rebuildSummaryFees(settlement.getId(), true); + applySummaryRequest(settlement.getId(), contractChanged ? List.of() : request.getSummaryFees()); + refreshSettlementAmount(settlement); + if (created) { + saveChange(settlement.getId(), "结算单基本信息", null, "新增", + "新增预结算单" + settlement.getPreSettlementNo(), ""); + } + return settlement.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDraft(Long id) { + PreSettlement settlement = loadExisting(id); + if (!STATUS_DRAFT.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅草稿状态的预结算单允许删除"); + } + releaseSourceDetails(settlement, listDetails(id)); + deleteChildren(id); + deleteLogic(List.of(id)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeDetail(Long id, Long detailId) { + PreSettlement settlement = loadEditable(id); + PreSettlementDetail detail = detailMapper.selectById(detailId); + if (detail == null || !Objects.equals(detail.getPreSettlementId(), id) + || Objects.equals(detail.getIsDeleted(), 1)) { + throw new ServiceException("预结算明细不存在"); + } + releaseSourceDetails(settlement, List.of(detail)); + detailFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementDetailFee::getPreSettlementDetailId, detailId)); + detailMapper.deleteById(detailId); + saveChange(id, "结算明细项", detail.getLineNo(), "删除", + "删除单据号" + detail.getDocumentNo(), ""); + renumberDetails(id); + rebuildSummaryFees(id, true); + refreshSettlementAmount(settlement); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(PreSettlementStatusRequest request) { + PreSettlement settlement = loadEditable(request.getId()); + validateBeforeSubmit(settlement); + settlement.setApprovalStatus(STATUS_REVIEWING); + settlement.setCurrentNode(Func.isEmpty(request.getCurrentNode()) ? "预结算审批" : request.getCurrentNode()); + settlement.setCurrentProcessor(Func.isEmpty(request.getCurrentProcessor()) ? "待处理" : request.getCurrentProcessor()); + updateById(settlement); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "提交预结算审批", request.getReason()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(PreSettlementStatusRequest request) { + PreSettlement settlement = loadReviewing(request.getId()); + settlement.setApprovalStatus(STATUS_APPROVED); + settlement.setCurrentNode("审批通过"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + settlement.setApprovedTime(LocalDateTime.now()); + updateById(settlement); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批通过", request.getReason()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnBill(PreSettlementStatusRequest request) { + PreSettlement settlement = loadReviewing(request.getId()); + String reason = requiredText(limitText(request.getReason(), 200, "驳回原因"), "驳回原因"); + settlement.setApprovalStatus(STATUS_RETURNED); + settlement.setCurrentNode("已驳回"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + updateById(settlement); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "预结算审批驳回", reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidBill(PreSettlementStatusRequest request) { + PreSettlement settlement = loadExisting(request.getId()); + String reason = requiredText(limitText(request.getReason(), 200, "作废原因"), "作废原因"); + if (!STATUS_APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的预结算单允许作废"); + } + if (Func.isNotEmpty(settlement.getFormalSettlementNo())) { + throw new ServiceException("已转正式结算的预结算单不可作废"); + } + long activeAdvanceCount = advanceMapper.selectCount(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, settlement.getId()) + .eq(PreSettlementAdvance::getIsDeleted, 0) + .ne(PreSettlementAdvance::getBillStatus, STATUS_VOIDED)); + if (activeAdvanceCount > 0) { + throw new ServiceException("该预结算单存在关联预付申请,请先作废预付申请"); + } + settlement.setApprovalStatus(STATUS_VOIDED); + settlement.setCurrentNode("已作废"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + settlement.setVoidReason(reason); + updateById(settlement); + releaseSourceDetails(settlement, listDetails(settlement.getId())); + saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废预结算单", reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void applyAdvance(PreSettlementAdvanceRequest request) { + PreSettlement settlement = loadExisting(request.getPreSettlementId()); + if (!STATUS_APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的应付预结算单允许发起预付申请"); + } + if (!"payable".equals(settlement.getSettlementType())) { + throw new ServiceException("仅应付预结算单允许发起预付申请"); + } + if (Func.isNotEmpty(settlement.getFormalSettlementNo())) { + throw new ServiceException("转正式结算后无法发起预付"); + } + BigDecimal appliedAmount = money(request.getAppliedAmount()); + if (appliedAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("申请预付金额必须大于0"); + } + BigDecimal availableAmount = money(settlement.getSettlementAmount()) + .subtract(money(settlement.getAdvanceAppliedAmount())); + if (appliedAmount.compareTo(availableAmount) > 0) { + throw new ServiceException("申请预付金额不能超过剩余可申请金额"); + } + PreSettlementAdvance advance = new PreSettlementAdvance(); + advance.setPreSettlementId(settlement.getId()); + advance.setAdvanceNo(nextAdvanceNo()); + advance.setAppliedAmount(appliedAmount); + advance.setPaidAmount(BigDecimal.ZERO.setScale(2)); + advance.setBillStatus(STATUS_REVIEWING); + advance.setKingdeeAdvanceNo(limitText(request.getKingdeeAdvanceNo(), 100, "金蝶预付单号")); + advanceMapper.insert(advance); + refreshAdvanceSummary(settlement); + saveChange(settlement.getId(), "预付信息", null, "新增", + "新增预付申请" + advance.getAdvanceNo() + ",申请金额" + appliedAmount, ""); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAdvancePaidAmount(Long advanceId, BigDecimal paidAmount, String kingdeeAdvanceNo) { + PreSettlementAdvance advance = advanceMapper.selectById(advanceId); + if (advance == null || Objects.equals(advance.getIsDeleted(), 1)) { + throw new ServiceException("预付申请不存在"); + } + if (STATUS_VOIDED.equals(advance.getBillStatus())) { + throw new ServiceException("已作废的预付申请不能回写付款金额"); + } + BigDecimal normalizedPaidAmount = money(paidAmount); + if (normalizedPaidAmount.compareTo(BigDecimal.ZERO) < 0 + || normalizedPaidAmount.compareTo(money(advance.getAppliedAmount())) > 0) { + throw new ServiceException("已付款金额必须在0与申请预付金额之间"); + } + advance.setPaidAmount(normalizedPaidAmount); + advance.setKingdeeAdvanceNo(limitText(kingdeeAdvanceNo, 100, "金蝶预付单号")); + advance.setBillStatus(normalizedPaidAmount.compareTo(money(advance.getAppliedAmount())) >= 0 ? "paid" : STATUS_APPROVED); + advanceMapper.updateById(advance); + PreSettlement settlement = loadExisting(advance.getPreSettlementId()); + refreshAdvanceSummary(settlement); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void voidAdvance(Long advanceId, String reason) { + PreSettlementAdvance advance = advanceMapper.selectById(advanceId); + if (advance == null || Objects.equals(advance.getIsDeleted(), 1)) { + throw new ServiceException("预付申请不存在"); + } + if (STATUS_VOIDED.equals(advance.getBillStatus())) { + throw new ServiceException("预付申请已作废,请勿重复操作"); + } + if ("paid".equals(advance.getBillStatus()) || money(advance.getPaidAmount()).compareTo(BigDecimal.ZERO) > 0) { + throw new ServiceException("已付款的预付申请不能作废"); + } + String voidReason = requiredText(limitText(reason, 200, "作废原因"), "作废原因"); + advance.setBillStatus(STATUS_VOIDED); + advanceMapper.updateById(advance); + PreSettlement settlement = loadExisting(advance.getPreSettlementId()); + refreshAdvanceSummary(settlement); + saveChange(settlement.getId(), "预付信息", null, "删除", + "作废预付申请" + advance.getAdvanceNo(), voidReason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String formalSettlement(Long id) { + PreSettlement settlement = loadExisting(id); + if (!STATUS_APPROVED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批通过的预结算单允许尾款结算"); + } + if (Func.isNotEmpty(settlement.getFormalSettlementNo())) { + throw new ServiceException("该预结算单已转正式结算"); + } + if (!"payable".equals(settlement.getSettlementType())) { + throw new ServiceException("仅应付预结算单允许发起尾款结算"); + } + long pendingAdvanceCount = advanceMapper.selectCount(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, id) + .eq(PreSettlementAdvance::getIsDeleted, 0) + .in(PreSettlementAdvance::getBillStatus, STATUS_REVIEWING, STATUS_APPROVED)); + if (pendingAdvanceCount > 0) { + throw new ServiceException("该预结算单存在在途预付申请,无法尾款结算"); + } + String formalSettlementNo = nextFormalSettlementNo(); + settlement.setFormalSettlementNo(formalSettlementNo); + settlement.setFormalSettledTime(LocalDateTime.now()); + settlement.setCurrentNode("已转正式结算"); + settlement.setCurrentProcessor(AuthUtil.getUserName()); + updateById(settlement); + for (PreSettlementDetail detail : listDetails(id)) { + ReceivablePayableDetail source = sourceDetailMapper.selectById(detail.getSourceDetailId()); + if (source != null && !Objects.equals(source.getIsDeleted(), 1)) { + source.setFormalSettlementNo(formalSettlementNo); + source.setSettlementStatus("formal_settled"); + sourceDetailMapper.updateById(source); + } + } + saveChange(id, "结算单基本信息", null, "调整", "转正式结算" + formalSettlementNo, ""); + return formalSettlementNo; + } + + @Override + public List detailFees(Long detailId) { + PreSettlementDetail detail = detailMapper.selectById(detailId); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) { + throw new ServiceException("预结算明细不存在"); + } + return detailFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementDetailFee::getPreSettlementDetailId, detailId) + .eq(PreSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(PreSettlementDetailFee::getCreateTime)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void adjustDetail(PreSettlementDetailAdjustRequest request) { + PreSettlementDetail detail = detailMapper.selectById(request.getDetailId()); + if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) { + throw new ServiceException("预结算明细不存在"); + } + PreSettlement settlement = loadEditable(detail.getPreSettlementId()); + if (Func.isEmpty(request.getRows())) { + throw new ServiceException("请填写需要调整的费用行"); + } + String changeReason = requiredText(limitRemark(request.getChangeReason(), 200), "调整原因"); + Map existingMap = detailFees(detail.getId()).stream() + .collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity())); + if (request.getRows().size() != existingMap.size()) { + throw new ServiceException("费用调整行数据不完整"); + } + List changes = new ArrayList<>(); + for (PreSettlementDetailAdjustRequest.FeeRow requestRow : request.getRows()) { + PreSettlementDetailFee fee = existingMap.get(requestRow.getId()); + if (fee == null) { + throw new ServiceException("存在无效的预结算费用行"); + } + BigDecimal beforeQuantity = fee.getTransportQuantity(); + BigDecimal beforeMileage = fee.getMileage(); + BigDecimal beforeUnitPrice = fee.getUnitPrice(); + BigDecimal beforeFreight = fee.getFreightAmount(); + BigDecimal beforeAmount = money(fee.getSettlementAmountTax()); + BigDecimal beforeNoTaxAmount = fee.getSettlementAmountNoTax(); + String beforeRemark = fee.getRemark(); + Map beforeFeeItems = parseFeeItems(fee.getFeeItemsJson()); + fee.setTransportQuantity(nonNegative(requestRow.getTransportQuantity(), "运输总量")); + fee.setMileage(nonNegative(requestRow.getMileage(), "里程")); + fee.setUnitPrice(nonNegative(requestRow.getUnitPrice(), "运输单价")); + fee.setFreightAmount(nonNegative(requestRow.getFreightAmount(), "运费")); + fee.setFeeItemsJson(JsonUtil.toJson(normalizeFeeItems(requestRow.getFeeItems()))); + BigDecimal afterAmount = requestRow.getSettlementAmountTax() == null + ? calculateFeeAmount(fee) : nonNegative(requestRow.getSettlementAmountTax(), "结算金额(含税)"); + fee.setSettlementAmountTax(afterAmount); + fee.setSettlementAmountNoTax(requestRow.getSettlementAmountNoTax() == null ? null + : nonNegative(requestRow.getSettlementAmountNoTax(), "结算金额(不含税)")); + fee.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount()))); + fee.setRemark(limitRemark(requestRow.getRemark(), 200)); + detailFeeMapper.updateById(fee); + String prefix = "【" + firstNotEmpty(fee.getCargoName(), fee.getLineNo()) + "】"; + appendChange(changes, prefix + "运输总量", beforeQuantity, fee.getTransportQuantity()); + appendChange(changes, prefix + "里程", beforeMileage, fee.getMileage()); + appendChange(changes, prefix + "运输单价", beforeUnitPrice, fee.getUnitPrice()); + appendChange(changes, prefix + "运费", beforeFreight, fee.getFreightAmount()); + Map afterFeeItems = parseFeeItems(fee.getFeeItemsJson()); + Set feeItemNames = new LinkedHashSet<>(beforeFeeItems.keySet()); + feeItemNames.addAll(afterFeeItems.keySet()); + feeItemNames.forEach(name -> appendChange(changes, prefix + name, + beforeFeeItems.get(name), afterFeeItems.get(name))); + appendChange(changes, prefix + "结算金额(含税)", beforeAmount, afterAmount); + appendChange(changes, prefix + "结算金额(不含税)", beforeNoTaxAmount, + fee.getSettlementAmountNoTax()); + appendTextChange(changes, prefix + "备注", beforeRemark, fee.getRemark()); + } + if (changes.isEmpty()) { + throw new ServiceException("未修改任何结算明细费用"); + } + refreshDetail(detail); + rebuildSummaryFees(settlement.getId(), true); + refreshSettlementAmount(settlement); + saveChange(settlement.getId(), "结算明细项", detail.getLineNo(), "调整", + String.join(";", changes), changeReason); + } + + @Override + public List> printTemplates(Long id) { + PreSettlement settlement = loadExisting(id); + if (STATUS_VOIDED.equals(settlement.getApprovalStatus())) { + throw new ServiceException("已作废的预结算单不能打印"); + } + return resolvePrintTemplates(settlement); + } + + private LambdaQueryWrapper buildQuery(PreSettlementVO query) { + return Wrappers.lambdaQuery() + .eq(PreSettlement::getIsDeleted, 0) + .like(Func.isNotEmpty(query.getPreSettlementNo()), PreSettlement::getPreSettlementNo, + query.getPreSettlementNo()) + .like(Func.isNotEmpty(query.getAdvanceNo()), PreSettlement::getAdvanceNo, query.getAdvanceNo()) + .like(Func.isNotEmpty(query.getProjectName()), PreSettlement::getProjectName, query.getProjectName()) + .like(Func.isNotEmpty(query.getDeptName()), PreSettlement::getDeptName, query.getDeptName()) + .like(Func.isNotEmpty(query.getContractName()), PreSettlement::getContractName, query.getContractName()) + .like(Func.isNotEmpty(query.getContractNo()), PreSettlement::getContractNo, query.getContractNo()) + .like(Func.isNotEmpty(query.getPayeeName()), PreSettlement::getPayeeName, query.getPayeeName()) + .like(Func.isNotEmpty(query.getPayerName()), PreSettlement::getPayerName, query.getPayerName()) + .eq(Func.isNotEmpty(query.getApprovalStatus()), PreSettlement::getApprovalStatus, + query.getApprovalStatus()) + .eq(Func.isNotEmpty(query.getSettlementType()), PreSettlement::getSettlementType, + query.getSettlementType()) + .ge(query.getCreateStartDate() != null, PreSettlement::getCreateTime, + query.getCreateStartDate() == null ? null : query.getCreateStartDate().atStartOfDay()) + .le(query.getCreateEndDate() != null, PreSettlement::getCreateTime, + query.getCreateEndDate() == null ? null : query.getCreateEndDate().plusDays(1).atStartOfDay()) + .orderByDesc(PreSettlement::getCreateTime); + } + + private Map candidateMap(ReceivablePayableDetail source) { + Map result = new LinkedHashMap<>(); + result.put("id", source.getId()); + result.put("documentNo", source.getDocumentNo()); + result.put("feeDate", source.getFeeDate()); + result.put("customerName", source.getCustomerName()); + result.put("departureAddress", waybillValue(source.getWaybillId(), Waybill::getDepartureAddress)); + result.put("arrivalAddress", waybillValue(source.getWaybillId(), Waybill::getArrivalAddress)); + result.put("contractName", source.getContractName()); + result.put("waybillNo", source.getWaybillNo()); + result.put("vehicleNo", source.getVehicleNo()); + result.put("transportType", source.getTransportType()); + result.put("cargoName", source.getCargoName()); + result.put("cargoType", source.getCargoType()); + result.put("batchNo", source.getBatchNo()); + result.put("totalAmount", money(source.getTotalAmount())); + result.put("currency", source.getCurrency()); + return result; + } + + private String waybillValue(Long waybillId, Function getter) { + if (waybillId == null) return ""; + Waybill waybill = waybillService.getById(waybillId); + return waybill == null ? "" : getter.apply(waybill); + } + + private void synchronizeDetails(PreSettlement settlement, List requestedIds) { + List distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList(); + List existingDetails = listDetails(settlement.getId()); + Set requestedSet = new LinkedHashSet<>(distinctIds); + List removed = existingDetails.stream() + .filter(detail -> !requestedSet.contains(detail.getSourceDetailId())).toList(); + if (!removed.isEmpty()) { + releaseSourceDetails(settlement, removed); + List removedDetailIds = removed.stream().map(PreSettlementDetail::getId).toList(); + detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, removedDetailIds)); + detailMapper.deleteBatchIds(removedDetailIds); + } + Set existingSourceIds = existingDetails.stream().map(PreSettlementDetail::getSourceDetailId) + .collect(Collectors.toSet()); + List addedIds = distinctIds.stream().filter(id -> !existingSourceIds.contains(id)).toList(); + if (!addedIds.isEmpty()) { + List sources = sourceDetailMapper.selectBatchIds(addedIds); + if (sources.size() != addedIds.size()) { + throw new ServiceException("存在无效的应收应付明细"); + } + boolean hasRetainedDetail = existingDetails.stream() + .anyMatch(detail -> requestedSet.contains(detail.getSourceDetailId())); + for (ReceivablePayableDetail source : sources) { + validateCandidate(settlement, source); + String sourceCurrency = Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency(); + if (!hasRetainedDetail) { + settlement.setCurrency(sourceCurrency); + hasRetainedDetail = true; + } else if (!Objects.equals(settlement.getCurrency(), sourceCurrency)) { + throw new ServiceException("同一预结算单仅允许选择相同币种的结算明细"); + } + PreSettlementDetail detail = copySourceDetail(settlement, source); + detailMapper.insert(detail); + copySourceFees(detail, source); + int affected = sourceDetailMapper.update(null, + Wrappers.lambdaUpdate() + .set(ReceivablePayableDetail::getPreSettlementNo, settlement.getPreSettlementNo()) + .set(ReceivablePayableDetail::getSettlementStatus, "pre_settled") + .eq(ReceivablePayableDetail::getId, source.getId()) + .eq(ReceivablePayableDetail::getSettlementStatus, "pending") + .and(query -> query.isNull(ReceivablePayableDetail::getPreSettlementNo) + .or().eq(ReceivablePayableDetail::getPreSettlementNo, ""))); + if (affected != 1) { + throw new ServiceException("单据" + source.getDocumentNo() + "已被其他预结算单选择"); + } + saveChange(settlement.getId(), "结算明细项", null, "新增", + "新增单据号" + source.getDocumentNo(), ""); + } + updateById(settlement); + } + renumberDetails(settlement.getId()); + } + + private void validateCandidate(PreSettlement settlement, ReceivablePayableDetail source) { + 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()) + || !Objects.equals(source.getDeptId(), settlement.getDeptId()) + || !List.of(settlement.getPayerName(), settlement.getPayeeName()).contains(source.getCustomerName())) { + throw new ServiceException("应收应付明细的项目、所属组织或客商与预结算单不一致"); + } + if (!"pending".equals(source.getSettlementStatus()) || Func.isNotEmpty(source.getPreSettlementNo()) + || Func.isNotEmpty(source.getFormalSettlementNo())) { + throw new ServiceException("单据" + source.getDocumentNo() + "已被结算或已关闭,不能重复选择"); + } + } + + private PreSettlementDetail copySourceDetail(PreSettlement settlement, ReceivablePayableDetail source) { + PreSettlementDetail detail = new PreSettlementDetail(); + detail.setPreSettlementId(settlement.getId()); + detail.setSourceDetailId(source.getId()); + detail.setDocumentNo(source.getDocumentNo()); + detail.setWaybillId(source.getWaybillId()); + detail.setWaybillNo(source.getWaybillNo()); + detail.setVehicleNo(source.getVehicleNo()); + detail.setTransportType(source.getTransportType()); + detail.setCargoName(source.getCargoName()); + detail.setCargoType(source.getCargoType()); + detail.setTransportQuantity(source.getTransportQuantity()); + detail.setQuantityUnit(source.getQuantityUnit()); + detail.setMileage(source.getMileage()); + detail.setBatchNo(source.getBatchNo()); + detail.setUnitPrice(source.getUnitPrice()); + detail.setFreightAmount(money(source.getFreightAmount())); + detail.setFeeItemsJson(source.getFeeItemsJson()); + detail.setOriginalAmount(money(source.getTotalAmount())); + detail.setAdjustAmount(BigDecimal.ZERO.setScale(2)); + detail.setSettlementAmountTax(money(source.getTotalAmount())); + detail.setCurrency(Func.isEmpty(source.getCurrency()) ? LOCAL_CURRENCY : source.getCurrency()); + detail.setRemark(source.getRemark()); + Waybill waybill = source.getWaybillId() == null ? null : waybillService.getById(source.getWaybillId()); + if (waybill != null) { + detail.setDepartureAddress(firstNotEmpty(waybill.getDepartureAddress(), waybill.getDepartureName())); + detail.setArrivalAddress(firstNotEmpty(waybill.getArrivalAddress(), waybill.getArrivalName())); + detail.setActualDepartureTime(waybill.getStartDate() == null ? null : waybill.getStartDate().atStartOfDay()); + detail.setActualCompletionTime(waybill.getEndDate() == null ? null : waybill.getEndDate().atStartOfDay()); + } + return detail; + } + + private void copySourceFees(PreSettlementDetail detail, ReceivablePayableDetail source) { + List sourceFees = sourceFeeMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceivablePayableCargoFee::getDetailId, source.getId()) + .eq(ReceivablePayableCargoFee::getIsDeleted, 0) + .orderByAsc(ReceivablePayableCargoFee::getCreateTime)); + if (sourceFees.isEmpty()) { + PreSettlementDetailFee fee = new PreSettlementDetailFee(); + fee.setPreSettlementDetailId(detail.getId()); + fee.setLineNo("0001"); + fee.setCargoName(source.getCargoName()); + fee.setCargoType(source.getCargoType()); + fee.setTransportQuantity(source.getTransportQuantity()); + fee.setQuantityUnit(source.getQuantityUnit()); + fee.setMileage(source.getMileage()); + fee.setUnitPrice(source.getUnitPrice()); + fee.setFreightAmount(money(source.getFreightAmount())); + fee.setFeeItemsJson(source.getFeeItemsJson()); + fee.setOriginalAmount(money(source.getTotalAmount())); + fee.setAdjustAmount(BigDecimal.ZERO.setScale(2)); + fee.setSettlementAmountTax(money(source.getTotalAmount())); + fee.setRemark(source.getRemark()); + detailFeeMapper.insert(fee); + return; + } + for (ReceivablePayableCargoFee sourceFee : sourceFees) { + PreSettlementDetailFee fee = Objects.requireNonNull( + BeanUtil.copyProperties(sourceFee, PreSettlementDetailFee.class)); + fee.setId(null); + fee.setPreSettlementDetailId(detail.getId()); + fee.setSourceFeeId(sourceFee.getId()); + fee.setSettlementAmountTax(money(sourceFee.getAfterAmount())); + fee.setSettlementAmountNoTax(null); + detailFeeMapper.insert(fee); + } + } + + private void rebuildSummaryFees(Long settlementId, boolean preserveAdjustments) { + PreSettlement settlement = loadExisting(settlementId); + Map feeTypeMap = contractFeeTypeMap(settlement.getContractId()); + List existingRows = listSummaryFees(settlementId); + Map existingGenerated = existingRows.stream() + .filter(row -> !Integer.valueOf(1).equals(row.getManualFlag())) + .collect(Collectors.toMap(this::summaryKey, Function.identity(), (left, right) -> left)); + Map aggregates = new LinkedHashMap<>(); + List detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList(); + List feeRows = detailIds.isEmpty() ? List.of() + : detailFeeMapper.selectList(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds) + .eq(PreSettlementDetailFee::getIsDeleted, 0) + .orderByAsc(PreSettlementDetailFee::getCreateTime)); + for (PreSettlementDetailFee feeRow : feeRows) { + Map feeItems = parseFeeItems(feeRow.getFeeItemsJson()); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + if (!containsFreight) { + aggregates.merge(summaryKey("物流配送", "运输费"), money(feeRow.getFreightAmount()), + BigDecimal::add); + } + feeItems.forEach((feeItem, amount) -> aggregates.merge( + summaryKey(feeTypeMap.getOrDefault(feeItem, + isFreightFeeItem(feeItem) ? "物流配送" : "其他费用"), feeItem), + money(amount), BigDecimal::add)); + } + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId) + .eq(PreSettlementSummaryFee::getManualFlag, 0)); + int lineNo = 1; + for (Map.Entry entry : aggregates.entrySet()) { + String[] keyParts = entry.getKey().split("\\|", 2); + PreSettlementSummaryFee old = existingGenerated.get(entry.getKey()); + PreSettlementSummaryFee row = new PreSettlementSummaryFee(); + row.setPreSettlementId(settlementId); + row.setLineNo(lineNo++); + row.setFeeType(keyParts[0]); + row.setFeeItem(keyParts.length > 1 ? keyParts[1] : ""); + row.setOriginalAmount(money(entry.getValue())); + row.setAdjustAmount(preserveAdjustments && old != null ? money(old.getAdjustAmount()) + : BigDecimal.ZERO.setScale(2)); + row.setSettlementAmount(row.getOriginalAmount().add(row.getAdjustAmount())); + if (row.getSettlementAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("结算合计金额不能小于0"); + } + row.setRemark(old == null ? "" : old.getRemark()); + row.setManualFlag(0); + summaryFeeMapper.insert(row); + } + renumberSummaryFees(settlementId); + } + + private void applySummaryRequest(Long settlementId, List requestRows) { + if (requestRows == null) return; + Map> allowedManualFees = new LinkedHashMap<>(); + if (requestRows.stream().anyMatch(row -> Integer.valueOf(1).equals(row.getManualFlag()))) { + for (Map option : feeOptions()) { + Set feeItems = new LinkedHashSet<>(); + if (option.get("feeItems") instanceof List values) { + values.forEach(value -> feeItems.add(String.valueOf(value))); + } + allowedManualFees.put(String.valueOf(option.get("feeType")), feeItems); + } + } + Map existingMap = listSummaryFees(settlementId).stream() + .collect(Collectors.toMap(PreSettlementSummaryFee::getId, Function.identity())); + List manualRows = existingMap.values().stream() + .filter(row -> Integer.valueOf(1).equals(row.getManualFlag())).toList(); + Set retainedManualIds = new LinkedHashSet<>(); + for (PreSettlementSaveRequest.SummaryFee requestRow : requestRows) { + if (Integer.valueOf(1).equals(requestRow.getManualFlag())) { + String feeType = requiredText(requestRow.getFeeType(), "费用类型"); + String feeItem = requiredText(requestRow.getFeeItem(), "费用项"); + if (!allowedManualFees.getOrDefault(feeType, Set.of()).contains(feeItem)) { + throw new ServiceException("费用类型与费用项不匹配或费用项已停用"); + } + PreSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId()); + if (row == null) { + row = existingMap.values().stream() + .filter(item -> Integer.valueOf(1).equals(item.getManualFlag())) + .filter(item -> Objects.equals(item.getFeeType(), requestRow.getFeeType()) + && Objects.equals(item.getFeeItem(), requestRow.getFeeItem())) + .findFirst().orElse(null); + } + if (row == null && requestRow.getId() == null) row = new PreSettlementSummaryFee(); + if (row == null || !Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的手工费用行"); + } + row.setPreSettlementId(settlementId); + row.setFeeType(feeType); + row.setFeeItem(feeItem); + row.setOriginalAmount(BigDecimal.ZERO.setScale(2)); + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + if (row.getAdjustAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("手工新增费用金额不能小于0"); + } + row.setSettlementAmount(row.getAdjustAmount()); + row.setRemark(limitRemark(requestRow.getRemark(), 50)); + row.setManualFlag(1); + if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row); + retainedManualIds.add(row.getId()); + saveChange(settlementId, "合计费用项", row.getLineNo(), requestRow.getId() == null ? "新增" : "调整", + row.getFeeItem() + "金额" + row.getSettlementAmount(), ""); + continue; + } + PreSettlementSummaryFee row = existingMap.get(requestRow.getId()); + if (row == null) { + row = existingMap.values().stream() + .filter(item -> !Integer.valueOf(1).equals(item.getManualFlag())) + .filter(item -> Objects.equals(item.getFeeType(), requestRow.getFeeType()) + && Objects.equals(item.getFeeItem(), requestRow.getFeeItem())) + .findFirst().orElse(null); + } + if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) { + throw new ServiceException("存在无效的结算合计行"); + } + BigDecimal before = money(row.getAdjustAmount()); + row.setAdjustAmount(money(requestRow.getAdjustAmount())); + row.setSettlementAmount(money(row.getOriginalAmount()).add(row.getAdjustAmount())); + if (row.getSettlementAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("结算金额不能小于0"); + } + row.setRemark(limitRemark(requestRow.getRemark(), 50)); + summaryFeeMapper.updateById(row); + if (before.compareTo(row.getAdjustAmount()) != 0) { + saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", + "【调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】", ""); + } + } + for (PreSettlementSummaryFee manualRow : manualRows) { + if (!retainedManualIds.contains(manualRow.getId())) { + summaryFeeMapper.deleteById(manualRow.getId()); + saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除", + "删除" + manualRow.getFeeItem() + "费用" + manualRow.getSettlementAmount(), ""); + } + } + renumberSummaryFees(settlementId); + } + + private void refreshDetail(PreSettlementDetail detail) { + List fees = detailFees(detail.getId()); + BigDecimal freightAmount = fees.stream().map(PreSettlementDetailFee::getFreightAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal originalAmount = fees.stream().map(PreSettlementDetailFee::getOriginalAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal settlementAmountTax = fees.stream().map(PreSettlementDetailFee::getSettlementAmountTax) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); + Map feeItems = new LinkedHashMap<>(); + fees.forEach(fee -> parseFeeItems(fee.getFeeItemsJson()).forEach((name, amount) -> + feeItems.merge(name, money(amount), BigDecimal::add))); + detail.setFreightAmount(money(freightAmount)); + detail.setOriginalAmount(money(originalAmount)); + detail.setAdjustAmount(money(settlementAmountTax.subtract(originalAmount))); + detail.setSettlementAmountTax(money(settlementAmountTax)); + detail.setFeeItemsJson(JsonUtil.toJson(feeItems)); + detailMapper.updateById(detail); + } + + private void refreshSettlementAmount(PreSettlement settlement) { + BigDecimal settlementAmount = listSummaryFees(settlement.getId()).stream() + .map(PreSettlementSummaryFee::getSettlementAmount).map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + settlement.setSettlementAmount(money(settlementAmount)); + BigDecimal rate = normalizeRate(settlement.getExchangeRate(), settlement.getCurrency()); + settlement.setExchangeRate(rate); + settlement.setLocalSettlementAmount(rate == null ? null : money(settlementAmount.multiply(rate))); + updateById(settlement); + } + + private void refreshAdvanceSummary(PreSettlement settlement) { + List advances = listAdvances(settlement.getId()).stream() + .filter(advance -> !STATUS_VOIDED.equals(advance.getBillStatus())).toList(); + settlement.setAdvanceNo(advances.stream().map(PreSettlementAdvance::getAdvanceNo) + .filter(Func::isNotEmpty).collect(Collectors.joining(","))); + settlement.setAdvanceAppliedAmount(advances.stream().map(PreSettlementAdvance::getAppliedAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + settlement.setAdvancePaidAmount(advances.stream().map(PreSettlementAdvance::getPaidAmount) + .map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)); + updateById(settlement); + } + + private void validateBeforeSubmit(PreSettlement settlement) { + if (listDetails(settlement.getId()).isEmpty()) { + throw new ServiceException("请至少选择一条结算明细"); + } + if (money(settlement.getSettlementAmount()).compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("结算金额必须大于0"); + } + if (!LOCAL_CURRENCY.equalsIgnoreCase(settlement.getCurrency())) { + if (settlement.getExchangeRateDate() == null) { + throw new ServiceException("外币结算必须选择汇率日期"); + } + if (settlement.getExchangeRate() == null || settlement.getExchangeRate().compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("外币结算必须填写大于0的结算汇率"); + } + } + } + + private PreSettlement loadExisting(Long id) { + PreSettlement settlement = id == null ? null : getById(id); + if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) { + throw new ServiceException("预结算单不存在"); + } + return settlement; + } + + private PreSettlement loadEditable(Long id) { + PreSettlement settlement = loadExisting(id); + if (!List.of(STATUS_DRAFT, STATUS_RETURNED).contains(settlement.getApprovalStatus())) { + throw new ServiceException("仅草稿或已驳回的预结算单允许编辑"); + } + return settlement; + } + + private PreSettlement loadReviewing(Long id) { + PreSettlement settlement = loadExisting(id); + if (!STATUS_REVIEWING.equals(settlement.getApprovalStatus())) { + throw new ServiceException("仅审批中的预结算单允许执行该操作"); + } + return settlement; + } + + private ContractManage loadAvailableContract(Long contractId) { + ContractManage contract = contractManageService.getById(contractId); + if (contract == null || Objects.equals(contract.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(contract.getApprovalStatus()) + || "terminated".equals(contract.getContractStage()) + || !List.of("客户合同", "承运商合同").contains(contract.getContractCategory())) { + throw new ServiceException("仅允许选择审批完成且未作废的合同"); + } + validateContractRelations(contract); + return contract; + } + + private boolean isAvailableContract(ContractManage contract) { + if (contract == null || Objects.equals(contract.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(contract.getApprovalStatus()) + || "terminated".equals(contract.getContractStage()) || contract.getProjectId() == null + || !List.of("客户合同", "承运商合同").contains(contract.getContractCategory())) { + return false; + } + org.springblade.transport.pojo.entity.ProjectApply project = projectApplyService.getById(contract.getProjectId()); + if (project == null || Objects.equals(project.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) { + return false; + } + return isApprovedCustomer(contract.getPartyA()) && isApprovedCustomer(contract.getPartyB()); + } + + private Map contractFeeTypeMap(Long contractId) { + ContractManage contract = contractId == null ? null : contractManageService.getById(contractId); + Map result = new LinkedHashMap<>(); + if (contract == null) return result; + for (Map plan : parseList(contract.getBillingPlanJson())) { + if (!(plan.get("rules") instanceof List rules)) continue; + for (Object value : rules) { + if (!(value instanceof Map raw)) continue; + Object feeItem = raw.get("feeItem"); + Object feeType = raw.get("feeType"); + if (!isBlank(feeItem) && !isBlank(feeType)) { + result.putIfAbsent(String.valueOf(feeItem), String.valueOf(feeType)); + } + } + } + return result; + } + + private boolean isApprovedCustomer(String customerName) { + if (Func.isEmpty(customerName)) return false; + return customerArchiveService.count(Wrappers.lambdaQuery() + .eq(CustomerArchive::getIsDeleted, 0) + .eq(CustomerArchive::getStatus, 1) + .eq(CustomerArchive::getApprovalStatus, "approved") + .eq(CustomerArchive::getFullName, customerName)) > 0; + } + + private void validateContractRelations(ContractManage contract) { + if (contract.getProjectId() == null) { + throw new ServiceException("合同未关联已审批项目"); + } + org.springblade.transport.pojo.entity.ProjectApply project = projectApplyService.getById(contract.getProjectId()); + if (project == null || Objects.equals(project.getIsDeleted(), 1) + || !List.of("approved", "change_approved").contains(project.getApprovalStatus())) { + throw new ServiceException("合同关联项目尚未完成审批"); + } + validateApprovedCustomer(contract.getPartyA()); + validateApprovedCustomer(contract.getPartyB()); + } + + private void validateApprovedCustomer(String customerName) { + if (!isApprovedCustomer(customerName)) { + throw new ServiceException("合同关联客商【" + customerName + "】尚未完成审批或已停用"); + } + } + + private void fillContract(PreSettlement settlement, ContractManage contract) { + settlement.setContractId(contract.getId()); + settlement.setContractNo(contract.getContractNo()); + settlement.setContractName(contract.getContractName()); + settlement.setProjectId(contract.getProjectId()); + settlement.setProjectName(contract.getProjectName()); + settlement.setDeptId(contract.getOrganizationId()); + settlement.setDeptName(contract.getOrganizationName()); + if ("receivable".equals(settlement.getSettlementType())) { + settlement.setPayerName(contract.getPartyB()); + settlement.setPayeeName(contract.getPartyA()); + } else { + settlement.setPayerName(contract.getPartyA()); + settlement.setPayeeName(contract.getPartyB()); + } + } + + private String resolveSettlementType(PreSettlementSaveRequest request, ContractManage contract) { + return contractSettlementType(contract); + } + + private String contractSettlementType(ContractManage contract) { + if ("客户合同".equals(contract.getContractCategory())) return "receivable"; + if ("承运商合同".equals(contract.getContractCategory())) return "payable"; + throw new ServiceException("合同类别不支持生成预结算单"); + } + + private void validateSettlementType(String settlementType) { + if (!List.of("receivable", "payable").contains(settlementType)) { + throw new ServiceException("结算类型不正确"); + } + } + + private void releaseSourceDetails(PreSettlement settlement, List details) { + for (PreSettlementDetail detail : details) { + ReceivablePayableDetail source = sourceDetailMapper.selectById(detail.getSourceDetailId()); + if (source != null && Objects.equals(source.getPreSettlementNo(), settlement.getPreSettlementNo()) + && Func.isEmpty(source.getFormalSettlementNo())) { + sourceDetailMapper.update(null, Wrappers.lambdaUpdate() + .set(ReceivablePayableDetail::getPreSettlementNo, null) + .set(ReceivablePayableDetail::getSettlementStatus, "pending") + .eq(ReceivablePayableDetail::getId, source.getId()) + .eq(ReceivablePayableDetail::getPreSettlementNo, settlement.getPreSettlementNo())); + } + } + } + + private void deleteChildren(Long settlementId) { + List detailIds = listDetails(settlementId).stream().map(PreSettlementDetail::getId).toList(); + if (!detailIds.isEmpty()) { + detailFeeMapper.delete(Wrappers.lambdaQuery() + .in(PreSettlementDetailFee::getPreSettlementDetailId, detailIds)); + } + detailMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementDetail::getPreSettlementId, settlementId)); + summaryFeeMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId)); + advanceMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, settlementId)); + changeRecordMapper.delete(Wrappers.lambdaQuery() + .eq(PreSettlementChangeRecord::getPreSettlementId, settlementId)); + } + + private List listDetails(Long settlementId) { + return detailMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementDetail::getPreSettlementId, settlementId) + .eq(PreSettlementDetail::getIsDeleted, 0) + .orderByAsc(PreSettlementDetail::getLineNo)); + } + + private List listSummaryFees(Long settlementId) { + return summaryFeeMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementSummaryFee::getPreSettlementId, settlementId) + .eq(PreSettlementSummaryFee::getIsDeleted, 0) + .orderByAsc(PreSettlementSummaryFee::getLineNo)); + } + + private List listAdvances(Long settlementId) { + return advanceMapper.selectList(Wrappers.lambdaQuery() + .eq(PreSettlementAdvance::getPreSettlementId, settlementId) + .eq(PreSettlementAdvance::getIsDeleted, 0) + .orderByDesc(PreSettlementAdvance::getCreateTime)); + } + + private void renumberDetails(Long settlementId) { + List details = listDetails(settlementId); + for (int index = 0; index < details.size(); index++) { + PreSettlementDetail detail = details.get(index); + detail.setLineNo(index + 1); + detailMapper.updateById(detail); + } + } + + private void renumberSummaryFees(Long settlementId) { + List rows = listSummaryFees(settlementId); + for (int index = 0; index < rows.size(); index++) { + PreSettlementSummaryFee row = rows.get(index); + row.setLineNo(index + 1); + summaryFeeMapper.updateById(row); + } + } + + private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType, + String content, String reason) { + PreSettlementChangeRecord record = new PreSettlementChangeRecord(); + record.setPreSettlementId(settlementId); + record.setChangeType(changeType); + record.setLineNo(lineNo); + record.setOperationType(operationType); + record.setChangeContent(content); + record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName()); + record.setChangeReason(reason); + record.setChangeTime(LocalDateTime.now()); + changeRecordMapper.insert(record); + } + + private List> resolvePrintTemplates(PreSettlement settlement) { + ContractManage contract = contractManageService.getById(settlement.getContractId()); + List> templates = new ArrayList<>(); + if (contract != null && Func.isNotEmpty(contract.getPreSettlementConfigJson())) { + try { + Object parsed = JsonUtil.parse(contract.getPreSettlementConfigJson(), Map.class); + if (parsed instanceof Map config && config.get("printTemplates") instanceof List rows) { + for (Object value : rows) { + if (value instanceof Map raw) { + Object nameValue = raw.get("name"); + String name = nameValue == null ? "" : String.valueOf(nameValue); + if (!name.isBlank()) { + templates.add(Map.of("value", name, "label", name)); + } + } + } + } + } catch (Exception ignored) { + // 兼容历史配置,使用系统默认打印模板。 + } + } + if (templates.isEmpty()) { + templates.add(Map.of("value", "default", "label", "默认模板(结算基本信息+结算合计)")); + } + return templates; + } + + private Map parseFeeItems(String json) { + Map result = new LinkedHashMap<>(); + if (Func.isEmpty(json)) return result; + try { + Object parsed = JsonUtil.parse(json, Map.class); + if (parsed instanceof Map map) { + map.forEach((key, value) -> result.put(String.valueOf(key), decimal(value))); + } + } catch (Exception ignored) { + // 历史脏数据不影响结算单展示。 + } + return result; + } + + private Map normalizeFeeItems(Map feeItems) { + Map result = new LinkedHashMap<>(); + if (feeItems == null) return result; + feeItems.forEach((key, value) -> result.put(key, nonNegative(value, key))); + return result; + } + + private BigDecimal calculateFeeAmount(PreSettlementDetailFee fee) { + Map feeItems = parseFeeItems(fee.getFeeItemsJson()); + BigDecimal feeItemTotal = feeItems.values().stream().map(this::money) + .reduce(BigDecimal.ZERO, BigDecimal::add); + boolean containsFreight = feeItems.keySet().stream().anyMatch(this::isFreightFeeItem); + return money(containsFreight ? feeItemTotal : money(fee.getFreightAmount()).add(feeItemTotal)); + } + + private boolean isFreightFeeItem(String name) { + return name != null && (name.contains("运费") || name.contains("运输费")); + } + + private BigDecimal nonNegative(BigDecimal value, String fieldName) { + BigDecimal normalized = value == null ? BigDecimal.ZERO : value; + if (normalized.compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException(fieldName + "不能小于0"); + } + return normalized; + } + + private BigDecimal normalizeRate(BigDecimal exchangeRate, String currency) { + if (LOCAL_CURRENCY.equalsIgnoreCase(Func.isEmpty(currency) ? LOCAL_CURRENCY : currency)) { + return BigDecimal.ONE.setScale(2, RoundingMode.HALF_UP); + } + if (exchangeRate == null) return null; + if (exchangeRate.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("结算汇率必须大于0"); + } + return exchangeRate.setScale(6, RoundingMode.HALF_UP); + } + + private BigDecimal money(BigDecimal value) { + return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP); + } + + private BigDecimal decimal(Object value) { + if (value == null || String.valueOf(value).isBlank()) return BigDecimal.ZERO; + try { + return new BigDecimal(String.valueOf(value)); + } catch (NumberFormatException exception) { + return BigDecimal.ZERO; + } + } + + @SuppressWarnings("unchecked") + private List> parseList(String json) { + if (Func.isEmpty(json)) return List.of(); + try { + Object parsed = JsonUtil.parse(json, List.class); + if (parsed instanceof List list) { + return list.stream().filter(Map.class::isInstance).map(item -> { + Map result = new LinkedHashMap<>(); + ((Map) item).forEach((key, value) -> result.put(String.valueOf(key), value)); + return result; + }).toList(); + } + } catch (Exception ignored) { + // 历史脏数据不影响费用类型映射。 + } + return List.of(); + } + + private boolean isBlank(Object value) { + return value == null || String.valueOf(value).isBlank(); + } + + private LocalDate parseDate(String value) { + try { + return Func.isEmpty(value) ? null : LocalDate.parse(value); + } catch (Exception exception) { + throw new ServiceException("日期格式应为yyyy-MM-dd"); + } + } + + private String requiredText(String value, String fieldName) { + if (value == null || value.trim().isEmpty()) { + throw new ServiceException(fieldName + "不能为空"); + } + return value.trim(); + } + + private String limitRemark(String value, int maxLength) { + if (value != null && value.length() > maxLength) { + throw new ServiceException("备注不能超过" + maxLength + "个字符"); + } + return value; + } + + private String limitText(String value, int maxLength, String fieldName) { + if (value != null && value.length() > maxLength) { + throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符"); + } + return value; + } + + private void appendChange(List changes, String fieldName, BigDecimal before, BigDecimal after) { + BigDecimal oldValue = money(before); + BigDecimal newValue = money(after); + if (oldValue.compareTo(newValue) != 0) { + changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】"); + } + } + + private void appendTextChange(List changes, String fieldName, String before, String after) { + String oldValue = before == null ? "" : before; + String newValue = after == null ? "" : after; + if (!Objects.equals(oldValue, newValue)) { + changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】"); + } + } + + private String firstNotEmpty(String first, String second) { + return Func.isNotEmpty(first) ? first : second; + } + + private String summaryKey(PreSettlementSummaryFee row) { + return summaryKey(row.getFeeType(), row.getFeeItem()); + } + + private String summaryKey(String feeType, String feeItem) { + return String.valueOf(feeType) + "|" + String.valueOf(feeItem); + } + + private synchronized String nextPreSettlementNo() { + return nextDailyCode("YJ", PreSettlement::getPreSettlementNo); + } + + private synchronized String nextFormalSettlementNo() { + return nextDailyCode("ZJ", PreSettlement::getFormalSettlementNo); + } + + private synchronized String nextAdvanceNo() { + String prefix = "YF" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + PreSettlementAdvance latest = advanceMapper.selectOne(Wrappers.lambdaQuery() + .likeRight(PreSettlementAdvance::getAdvanceNo, prefix) + .orderByDesc(PreSettlementAdvance::getAdvanceNo) + .last("limit 1")); + return prefix + String.format("%05d", nextSequence(latest == null ? null : latest.getAdvanceNo(), prefix)); + } + + private String nextDailyCode(String code, Function getter) { + String prefix = code + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + PreSettlement latest = list(Wrappers.lambdaQuery() + .and(query -> query.likeRight(PreSettlement::getPreSettlementNo, prefix) + .or().likeRight(PreSettlement::getFormalSettlementNo, prefix)) + .orderByDesc(PreSettlement::getCreateTime) + .last("limit 1")).stream().max(Comparator.comparing(item -> { + String value = getter.apply(item); + return value == null ? "" : value; + })).orElse(null); + String latestCode = latest == null ? null : getter.apply(latest); + return prefix + String.format("%05d", nextSequence(latestCode, prefix)); + } + + private int nextSequence(String latestCode, String prefix) { + if (latestCode == null || !latestCode.startsWith(prefix)) return 1; + try { + return Integer.parseInt(latestCode.substring(prefix.length())) + 1; + } catch (NumberFormatException exception) { + return 1; + } + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java new file mode 100644 index 0000000..ed750e1 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/FormalSettlementWrapper.java @@ -0,0 +1,45 @@ +/** + * 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.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.FormalSettlement; +import org.springblade.transport.pojo.vo.FormalSettlementVO; + +import java.util.Objects; + +/** + * 正式结算单包装类 + * + * @author Chill + */ +public class FormalSettlementWrapper extends BaseEntityWrapper { + + public static FormalSettlementWrapper build() { + return new FormalSettlementWrapper(); + } + + @Override + public FormalSettlementVO entityVO(FormalSettlement entity) { + FormalSettlementVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, FormalSettlementVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付"); + vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> entity.getApprovalStatus(); + }); + return vo; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java new file mode 100644 index 0000000..6832c40 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PreSettlementWrapper.java @@ -0,0 +1,51 @@ +/** + * 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.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.PreSettlement; +import org.springblade.transport.pojo.vo.PreSettlementVO; + +import java.util.Objects; + +/** + * 预结算单包装类 + * + * @author Chill + */ +public class PreSettlementWrapper extends BaseEntityWrapper { + + public static PreSettlementWrapper build() { + return new PreSettlementWrapper(); + } + + @Override + public PreSettlementVO entityVO(PreSettlement entity) { + PreSettlementVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, PreSettlementVO.class)); + vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); + vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser())); + vo.setApprovalStatusName(approvalStatusName(entity.getApprovalStatus())); + vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付"); + return vo; + } + + private String approvalStatusName(String status) { + return switch (status == null ? "" : status) { + case "draft" -> "草稿"; + case "reviewing" -> "审批中"; + case "approved" -> "审批通过"; + case "returned" -> "已驳回"; + case "voided" -> "已作废"; + default -> status; + }; + } + +} diff --git a/doc/sql/transport/blade_formal_settlement_20260818.sql b/doc/sql/transport/blade_formal_settlement_20260818.sql new file mode 100644 index 0000000..bc05c11 --- /dev/null +++ b/doc/sql/transport/blade_formal_settlement_20260818.sql @@ -0,0 +1,90 @@ +-- 结算管理 / 正式结算单 + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement` ( + `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_no` varchar(100) NOT NULL COMMENT '正式结算单号', `source_type` varchar(30) NOT NULL DEFAULT '预结算合并', + `settlement_type` 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) NOT NULL, + `contract_no` varchar(100) DEFAULT NULL, `contract_name` varchar(100) NOT NULL, `payer_name` varchar(200) DEFAULT NULL, + `payee_name` varchar(200) DEFAULT NULL, `currency` varchar(20) NOT NULL DEFAULT 'RMB', `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `local_currency` varchar(20) NOT NULL DEFAULT 'RMB', `local_settlement_amount` decimal(18,2) DEFAULT NULL, + `applied_payment_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请付款金额含预付', + `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已收已付合计', + `exchange_rate_date` date DEFAULT NULL, `exchange_rate` decimal(18,6) DEFAULT NULL, + `invoice_status` varchar(30) NOT NULL DEFAULT 'unreceived', `payment_status` varchar(30) NOT NULL DEFAULT 'unpaid', + `approval_status` varchar(30) NOT NULL DEFAULT 'draft', `current_node` varchar(100) DEFAULT NULL, `current_processor` varchar(200) DEFAULT NULL, + `kingdee_bill_no` varchar(100) DEFAULT NULL, `kingdee_sync_status` varchar(30) NOT NULL DEFAULT 'unsynced', + `attachments_json` longtext, `remark` varchar(200) DEFAULT NULL, `approved_time` datetime DEFAULT NULL, `synced_time` datetime DEFAULT NULL, + `void_reason` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_formal_settlement_no` (`tenant_id`,`formal_settlement_no`), + 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_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, + `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_id` bigint(20) NOT NULL, `pre_settlement_id` bigint(20) NOT NULL, `pre_settlement_no` varchar(100) NOT NULL, + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `advance_applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `advance_paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00', PRIMARY KEY (`id`), + KEY `idx_formal_source_pre` (`pre_settlement_id`), KEY `idx_formal_source_bill` (`formal_settlement_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算来源预结算'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_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', + `formal_settlement_id` bigint(20) NOT NULL, `source_pre_settlement_id` bigint(20) DEFAULT NULL, + `source_pre_settlement_detail_id` bigint(20) DEFAULT NULL, `source_detail_id` bigint(20) NOT NULL, `line_no` int(11) NOT NULL, + `document_no` varchar(100) DEFAULT NULL, `waybill_id` bigint(20) 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, `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) NOT NULL DEFAULT '0.00', `fee_items_json` longtext, + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL, + `currency` varchar(20) NOT NULL DEFAULT 'RMB', `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), + KEY `idx_formal_detail_bill` (`formal_settlement_id`), KEY `idx_formal_detail_source` (`source_detail_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算明细快照'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_detail_fee` ( + `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', + `formal_settlement_detail_id` bigint(20) NOT NULL, `source_fee_id` bigint(20) DEFAULT NULL, `line_no` varchar(30) DEFAULT NULL, + `cargo_name` varchar(100) DEFAULT NULL, `cargo_type` varchar(100) DEFAULT NULL, `transport_quantity` decimal(18,6) DEFAULT NULL, + `quantity_unit` varchar(50) DEFAULT NULL, `mileage` decimal(18,2) DEFAULT NULL, `unit_price` decimal(18,2) DEFAULT NULL, + `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `fee_items_json` longtext, `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00', + `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), + KEY `idx_formal_detail_fee_detail` (`formal_settlement_detail_id`), KEY `idx_formal_detail_fee_source` (`source_fee_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='正式结算货物费用快照'; + +CREATE TABLE IF NOT EXISTS `blade_formal_settlement_payment` ( + `id` bigint(20) NOT NULL, `tenant_id` varchar(12) NOT NULL DEFAULT '000000', `create_user` bigint(20) DEFAULT NULL, + `create_dept` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint(20) DEFAULT NULL, + `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0', + `formal_settlement_id` bigint(20) NOT NULL, `payment_no` varchar(100) NOT NULL, `payment_type` varchar(30) NOT NULL DEFAULT 'final', + `applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00', `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00', + `bill_status` varchar(30) NOT NULL DEFAULT 'reviewing', `kingdee_bill_no` varchar(100) DEFAULT NULL, `remark` varchar(200) DEFAULT NULL, + PRIMARY KEY (`id`), UNIQUE KEY `uk_formal_payment_no` (`tenant_id`,`payment_no`), + KEY `idx_formal_payment_bill` (`formal_settlement_id`), KEY `idx_formal_payment_status` (`bill_status`) +) 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 +(2090000000000001040,2090000000000001000,'formal_settlement','正式结算单','formal_settlement','/settlement/formal-settlement','iconfont icon-caidanguanli',4,1,0,1,NULL,'',0), +(2090000000000001041,2090000000000001040,'formal_settlement_view','查看','formal_settlement_view','','',1,2,0,1,NULL,'',0), +(2090000000000001042,2090000000000001040,'formal_settlement_add','新增','formal_settlement_add','','',2,2,0,1,NULL,'',0), +(2090000000000001043,2090000000000001040,'formal_settlement_edit','编辑','formal_settlement_edit','','',3,2,0,1,NULL,'',0), +(2090000000000001044,2090000000000001040,'formal_settlement_delete','删除','formal_settlement_delete','','',4,2,0,1,NULL,'',0), +(2090000000000001045,2090000000000001040,'formal_settlement_submit','提交审批','formal_settlement_submit','','',5,2,0,1,NULL,'',0), +(2090000000000001046,2090000000000001040,'formal_settlement_approve','审批','formal_settlement_approve','','',6,2,0,1,NULL,'',0), +(2090000000000001047,2090000000000001040,'formal_settlement_sync','同步金蝶','formal_settlement_sync','','',7,2,0,1,NULL,'',0), +(2090000000000001048,2090000000000001040,'formal_settlement_print','打印','formal_settlement_print','','',8,2,0,1,NULL,'',0), +(2090000000000001049,2090000000000001040,'formal_settlement_export','导出','formal_settlement_export','','',9,2,0,1,NULL,'',0), +(2090000000000001050,2090000000000001040,'formal_settlement_void','作废','formal_settlement_void','','',10,2,0,1,NULL,'',0), +(2090000000000001051,2090000000000001040,'formal_settlement_adjust','明细调整','formal_settlement_adjust','','',11,2,0,1,NULL,'',0), +(2090000000000001052,2090000000000001040,'formal_settlement_payment','付款申请','formal_settlement_payment','','',12,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0; diff --git a/doc/sql/transport/blade_pre_settlement_20260818.sql b/doc/sql/transport/blade_pre_settlement_20260818.sql new file mode 100644 index 0000000..cc3b761 --- /dev/null +++ b/doc/sql/transport/blade_pre_settlement_20260818.sql @@ -0,0 +1,214 @@ +-- 结算管理 / 预结算 + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement` ( + `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 '是否已删除', + `pre_settlement_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '预结算单号', + `source_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '应收应付' COMMENT '来源', + `settlement_type` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'payable' COMMENT '结算类型', + `project_id` bigint(20) DEFAULT NULL COMMENT '项目ID', + `project_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目名称', + `dept_id` bigint(20) DEFAULT NULL COMMENT '所属组织ID', + `dept_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所属组织', + `contract_id` bigint(20) NOT NULL COMMENT '合同ID', + `contract_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同编号', + `contract_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '合同名称', + `payer_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '付款方', + `payee_name` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '收款方', + `currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '结算币种', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `local_currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '本位币', + `local_settlement_amount` decimal(18,2) DEFAULT NULL COMMENT '本位币合计', + `exchange_rate_date` date DEFAULT NULL COMMENT '汇率日期', + `exchange_rate` decimal(18,6) DEFAULT NULL COMMENT '结算汇率', + `approval_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'draft' COMMENT '审核状态', + `current_node` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前节点', + `current_processor` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前处理人', + `advance_no` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '预付单号', + `advance_applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请预付金额', + `advance_paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已付款金额', + `formal_settlement_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '正式结算单号', + `attachments_json` longtext COLLATE utf8mb4_general_ci COMMENT '附件JSON', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `approved_time` datetime DEFAULT NULL COMMENT '审核通过时间', + `formal_settled_time` datetime DEFAULT NULL COMMENT '正式结算时间', + `void_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '作废原因', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_pre_settlement_no` (`tenant_id`,`pre_settlement_no`) USING BTREE, + KEY `idx_pre_settlement_contract` (`contract_id`) USING BTREE, + KEY `idx_pre_settlement_project` (`project_id`) USING BTREE, + KEY `idx_pre_settlement_approval` (`approval_status`) USING BTREE, + KEY `idx_pre_settlement_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算单'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_detail` ( + `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 '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `source_detail_id` bigint(20) NOT NULL COMMENT '应收应付明细ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `document_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '单据号', + `waybill_id` bigint(20) DEFAULT NULL COMMENT '运单ID', + `waybill_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运单号', + `vehicle_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '车号', + `departure_address` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发货地址', + `arrival_address` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '到货地址', + `actual_departure_time` datetime DEFAULT NULL COMMENT '实际发货时间', + `actual_completion_time` datetime DEFAULT NULL COMMENT '实际完成时间', + `transport_type` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运输类型', + `cargo_name` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物名称', + `cargo_type` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物类型', + `transport_quantity` decimal(18,6) DEFAULT NULL COMMENT '运输总量', + `quantity_unit` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数量单位', + `mileage` decimal(18,2) DEFAULT NULL COMMENT '里程', + `batch_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '批次号', + `unit_price` decimal(18,2) DEFAULT NULL COMMENT '运输单价', + `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '运费', + `fee_items_json` longtext COLLATE utf8mb4_general_ci COMMENT '费用项JSON', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额(含税)', + `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL COMMENT '结算金额(不含税)', + `currency` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'RMB' COMMENT '币种', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_source_detail` (`source_detail_id`) USING BTREE, + KEY `idx_pre_settlement_detail_bill` (`pre_settlement_id`) USING BTREE, + KEY `idx_pre_settlement_detail_waybill` (`waybill_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算明细'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_detail_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_detail_id` bigint(20) NOT NULL COMMENT '预结算明细ID', + `source_fee_id` bigint(20) DEFAULT NULL COMMENT '源费用行ID', + `line_no` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '行号', + `cargo_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物名称', + `cargo_type` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物类型', + `transport_quantity` decimal(18,6) DEFAULT NULL COMMENT '运输量', + `quantity_unit` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数量单位', + `mileage` decimal(18,2) DEFAULT NULL COMMENT '里程', + `unit_price` decimal(18,2) DEFAULT NULL COMMENT '运输单价', + `freight_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '运费', + `fee_items_json` longtext COLLATE utf8mb4_general_ci COMMENT '费用项JSON', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount_tax` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额(含税)', + `settlement_amount_no_tax` decimal(18,2) DEFAULT NULL COMMENT '结算金额(不含税)', + `remark` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_detail_fee_detail` (`pre_settlement_detail_id`) USING BTREE, + KEY `idx_pre_settlement_detail_fee_source` (`source_fee_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算明细费用快照'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_summary_fee` ( + `id` bigint(20) NOT NULL COMMENT '主键', + `tenant_id` varchar(12) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '000000' COMMENT '租户ID', + `create_user` bigint(20) DEFAULT NULL COMMENT '创建人', + `create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_user` bigint(20) DEFAULT NULL COMMENT '修改人', + `update_time` datetime DEFAULT NULL COMMENT '修改时间', + `status` int(11) DEFAULT '1' COMMENT '状态', + `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `line_no` int(11) NOT NULL COMMENT '行号', + `fee_type` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用类型', + `fee_item` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '费用项', + `original_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '原金额', + `adjust_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额', + `settlement_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '结算金额', + `remark` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `manual_flag` int(11) NOT NULL DEFAULT '0' COMMENT '是否手工添加', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_summary_bill` (`pre_settlement_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算合计费用'; + +CREATE TABLE IF NOT EXISTS `blade_pre_settlement_advance` ( + `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 '是否已删除', + `pre_settlement_id` bigint(20) NOT NULL COMMENT '预结算单ID', + `advance_no` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '预付单号', + `applied_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '申请预付金额', + `paid_amount` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '已付款金额', + `bill_status` varchar(30) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'reviewing' COMMENT '单据状态', + `kingdee_advance_no` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶预付单号', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_pre_settlement_advance_no` (`tenant_id`,`advance_no`) USING BTREE, + KEY `idx_pre_settlement_advance_bill` (`pre_settlement_id`) USING BTREE, + KEY `idx_pre_settlement_advance_status` (`bill_status`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='预结算预付记录'; + +CREATE TABLE IF NOT EXISTS `blade_pre_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 '是否已删除', + `pre_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 '变更内容', + `operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人', + `change_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因', + `change_time` datetime NOT NULL COMMENT '变更时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_pre_settlement_change_bill` (`pre_settlement_id`) USING BTREE, + KEY `idx_pre_settlement_change_time` (`change_time`) USING BTREE +) 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 +(2090000000000001020, 2090000000000001000, 'pre_settlement', '预结算', 'pre_settlement', '/settlement/pre-settlement', 'iconfont icon-caidanguanli', 3, 1, 0, 1, NULL, '', 0), +(2090000000000001021, 2090000000000001020, 'pre_settlement_view', '查看', 'pre_settlement_view', '', '', 1, 2, 0, 1, NULL, '', 0), +(2090000000000001022, 2090000000000001020, 'pre_settlement_add', '新增', 'pre_settlement_add', '', '', 2, 2, 0, 1, NULL, '', 0), +(2090000000000001023, 2090000000000001020, 'pre_settlement_edit', '编辑', 'pre_settlement_edit', '', '', 3, 2, 0, 1, NULL, '', 0), +(2090000000000001024, 2090000000000001020, 'pre_settlement_delete', '删除', 'pre_settlement_delete', '', '', 4, 2, 0, 1, NULL, '', 0), +(2090000000000001025, 2090000000000001020, 'pre_settlement_submit', '提交审批', 'pre_settlement_submit', '', '', 5, 2, 0, 1, NULL, '', 0), +(2090000000000001026, 2090000000000001020, 'pre_settlement_approve', '审批', 'pre_settlement_approve', '', '', 6, 2, 0, 1, NULL, '', 0), +(2090000000000001027, 2090000000000001020, 'pre_settlement_advance', '预付申请', 'pre_settlement_advance', '', '', 7, 2, 0, 1, NULL, '', 0), +(2090000000000001028, 2090000000000001020, 'pre_settlement_formal', '尾款结算', 'pre_settlement_formal', '', '', 8, 2, 0, 1, NULL, '', 0), +(2090000000000001029, 2090000000000001020, 'pre_settlement_print', '打印结算单', 'pre_settlement_print', '', '', 9, 2, 0, 1, NULL, '', 0), +(2090000000000001030, 2090000000000001020, 'pre_settlement_export', '导出', 'pre_settlement_export', '', '', 10, 2, 0, 1, NULL, '', 0), +(2090000000000001031, 2090000000000001020, 'pre_settlement_adjust', '明细调整', 'pre_settlement_adjust', '', '', 11, 2, 0, 1, NULL, '', 0), +(2090000000000001032, 2090000000000001020, 'pre_settlement_void', '作废', 'pre_settlement_void', '', '', 12, 2, 0, 1, NULL, '', 0) +ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `path` = VALUES(`path`), + `sort` = VALUES(`sort`), + `is_deleted` = 0;