Merge remote-tracking branch 'refs/remotes/origin/master' into feature/20261016-settlement-adjustment-kingdee
This commit is contained in:
+15
@@ -72,4 +72,19 @@ public class OaOrgSyncPageVO implements Serializable {
|
||||
|
||||
@Schema(description = "全量同步结束后原始数据层级 JSON 文件路径")
|
||||
private String dumpFilePath;
|
||||
|
||||
@Schema(description = "当前同步的公司 OA 主键(部门阶段按公司轮询时使用)")
|
||||
private String companyOaId;
|
||||
|
||||
@Schema(description = "当前公司序号,从 1 开始")
|
||||
private Integer companyIndex;
|
||||
|
||||
@Schema(description = "待同步公司总数")
|
||||
private Integer companyTotal;
|
||||
|
||||
@Schema(description = "当前公司的部门是否已同步完")
|
||||
private Boolean companyFinished;
|
||||
|
||||
@Schema(description = "下一公司 OA 主键;当前公司完成后回传,前端据此切换并重置页码")
|
||||
private String nextCompanyOaId;
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ public class PreSettlementDetailAdjustRequest implements Serializable {
|
||||
@Schema(description = "预结算明细ID")
|
||||
private Long detailId;
|
||||
|
||||
@Schema(description = "调整原因")
|
||||
@Schema(description = "备注(写入结算明细备注,非必填)")
|
||||
private String changeReason;
|
||||
|
||||
@Schema(description = "明细费用行")
|
||||
|
||||
+4
@@ -25,6 +25,10 @@ public class FormalSettlementChangeRecord extends TenantEntity {
|
||||
private Integer lineNo;
|
||||
private String operationType;
|
||||
private String changeContent;
|
||||
/** 变更前数据JSON */
|
||||
private String beforeData;
|
||||
/** 变更后数据JSON */
|
||||
private String afterData;
|
||||
private String changeReason;
|
||||
private String operatorName;
|
||||
private LocalDateTime changeTime;
|
||||
|
||||
+8
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
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;
|
||||
@@ -123,6 +125,12 @@ public class FormalSettlementDetail extends TenantEntity {
|
||||
* 批次号
|
||||
*/
|
||||
private String batchNo;
|
||||
/**
|
||||
* 计费方式(来源应收应付货物费用计费类型,非本表字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "计费方式")
|
||||
private String billingType;
|
||||
/**
|
||||
* 运输单价
|
||||
*/
|
||||
|
||||
+8
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
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;
|
||||
@@ -70,6 +72,12 @@ public class FormalSettlementDetailFee extends TenantEntity {
|
||||
* 费用项JSON
|
||||
*/
|
||||
private String feeItemsJson;
|
||||
/**
|
||||
* 命中计费规则JSON(来源费用带回,非本表字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "命中计费规则JSON")
|
||||
private String billingRulesJson;
|
||||
/**
|
||||
* 原金额
|
||||
*/
|
||||
|
||||
+3
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
package org.springblade.transport.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -41,6 +43,7 @@ public class TransportReconciliationExternal extends TenantEntity {
|
||||
private BigDecimal freightAmount;
|
||||
private String feeItemsJson;
|
||||
private BigDecimal settlementAmount;
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
private Boolean suspectedDuplicate;
|
||||
private String matchStatus;
|
||||
private Long matchedInternalId;
|
||||
|
||||
+2
@@ -27,4 +27,6 @@ public class PaymentApplicationReferenceAmountVO implements Serializable {
|
||||
@Schema(description = "结算金额") private BigDecimal settlementAmount;
|
||||
@Schema(description = "累计申请付款金额") private BigDecimal cumulativeAppliedAmount;
|
||||
@Schema(description = "可付款金额") private BigDecimal payableAmount;
|
||||
/** 该结算单当前为第几次付款(含本次,已作废不计) */
|
||||
@Schema(description = "付款次序") private Integer paymentSequence;
|
||||
}
|
||||
|
||||
+4
-3
@@ -204,7 +204,7 @@ public class DeptController extends BladeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OA按页同步部门(需先完成公司同步)
|
||||
* 从OA按页同步部门(需先完成公司同步;按公司 oaId 轮询查询)
|
||||
*/
|
||||
@IsAdmin
|
||||
@PostMapping("/sync-oa-department")
|
||||
@@ -214,8 +214,9 @@ public class DeptController extends BladeController {
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size,
|
||||
@RequestParam(defaultValue = "false") Boolean incremental,
|
||||
@RequestParam(required = false) String modifiedSince) {
|
||||
return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince));
|
||||
@RequestParam(required = false) String modifiedSince,
|
||||
@RequestParam(required = false) String companyOaId) {
|
||||
return R.data(oaSyncService.syncDepartmentPage(current, size, Boolean.TRUE.equals(incremental), modifiedSince, companyOaId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-3
@@ -52,13 +52,14 @@ public interface IOASyncService {
|
||||
OaOrgSyncPageVO syncCompanyPage(int current, int size, boolean incremental, String modifiedSince);
|
||||
|
||||
/**
|
||||
* 按页从 OA 部门接口同步部门;最后一页完成后更新祖级列表
|
||||
* 按页从 OA 部门接口同步部门;须先完成公司同步,再按公司 oaId 轮询查询部门
|
||||
*
|
||||
* @param current 当前页,从 1 开始
|
||||
* @param current 当前页,从 1 开始(相对当前公司)
|
||||
* @param size 每页条数
|
||||
* @param incremental 是否增量同步(按本地最大 oaModified 过滤 OA)
|
||||
* @param modifiedSince 增量起始时间;首屏可空,后续分页回传首屏返回值
|
||||
* @param companyOaId 当前公司 OA 主键;首屏可空(自动取第一家),后续回传
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince);
|
||||
OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince, String companyOaId);
|
||||
}
|
||||
|
||||
+63
-18
@@ -1039,7 +1039,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
boolean isCompany = this.isOaCompany(dept);
|
||||
boolean parentExists = isCompany
|
||||
? companyByOaId.containsKey(parentOaId)
|
||||
: departmentByOaId.containsKey(parentOaId) || companyByOaId.containsKey(parentOaId);
|
||||
: this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId) != null;
|
||||
// 全量数据整理后仍找不到上级:公司、部门一律删除,不改挂保留
|
||||
if (!parentExists) {
|
||||
deleteIds.add(dept.getId());
|
||||
@@ -1063,7 +1063,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
Dept parent = this.isOaCompany(dept)
|
||||
? companyByOaId.get(parentOaId)
|
||||
: Optional.ofNullable(departmentByOaId.get(parentOaId)).orElse(companyByOaId.get(parentOaId));
|
||||
: this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId);
|
||||
if (parent != null && deleteIds.contains(parent.getId())) {
|
||||
deleteIds.add(dept.getId());
|
||||
oaCascadeChanged = true;
|
||||
@@ -1175,7 +1175,10 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 OA 上级主键解析本系统父级
|
||||
* 根据 OA 上级主键解析本系统父级。
|
||||
* <p>
|
||||
* OA 公司与部门主键同属数字空间(如公司 id=19「运输事业部」与部门 id=19「投资发展部」),
|
||||
* 解析部门上级时必须按「同属公司」消歧,禁止跨公司误挂。
|
||||
*/
|
||||
private OaParentLink resolveOaParentLink(Dept dept, Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
@@ -1202,21 +1205,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
log.warn("部门缺少上级OA主键,跳过层级重建 oaId={} deptId={}", dept.getOaId(), dept.getId());
|
||||
return null;
|
||||
}
|
||||
boolean parentIsDepartment = StringUtil.isNotBlank(dept.getParentCode())
|
||||
&& dept.getParentCode().startsWith(OAConvertConstant.DEPARTMENT_OA_PREFIX);
|
||||
boolean parentIsCompany = StringUtil.isNotBlank(dept.getParentCode())
|
||||
&& dept.getParentCode().startsWith(OAConvertConstant.COMPANY_OA_PREFIX);
|
||||
Dept parent;
|
||||
if (parentIsDepartment) {
|
||||
parent = departmentByOaId.get(parentOaId);
|
||||
} else if (parentIsCompany) {
|
||||
parent = companyByOaId.get(parentOaId);
|
||||
} else {
|
||||
parent = departmentByOaId.get(parentOaId);
|
||||
if (parent == null) {
|
||||
parent = companyByOaId.get(parentOaId);
|
||||
}
|
||||
}
|
||||
Dept parent = this.resolveOaDepartmentParent(dept, parentOaId, companyByOaId, departmentByOaId);
|
||||
if (parent == null || parent.getId() == null || Objects.equals(parent.getId(), dept.getId())) {
|
||||
log.warn("未找到上级组织,跳过层级重建 oaId={} oaSupSubComId={} deptId={}",
|
||||
dept.getOaId(), parentOaId, dept.getId());
|
||||
@@ -1227,6 +1216,62 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
|
||||
return new OaParentLink(parent.getId(), this.resolveParentCode(parent, prefix, parentOaId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析部门的 OA 上级:优先同公司下的上级部门,其次所属/同号公司;避免公司与部门 oaId 撞号误挂。
|
||||
*/
|
||||
private Dept resolveOaDepartmentParent(Dept dept, String parentOaId,
|
||||
Map<String, Dept> companyByOaId, Map<String, Dept> departmentByOaId) {
|
||||
Dept belongCompany = this.findBelongCompany(dept, companyByOaId);
|
||||
// 根部门:oaSupSubComId 存的是所属公司 OA 主键
|
||||
if (belongCompany != null && StringUtil.equals(parentOaId, belongCompany.getOaId())) {
|
||||
return belongCompany;
|
||||
}
|
||||
Dept parentDept = departmentByOaId.get(parentOaId);
|
||||
if (parentDept != null && this.isSameOaCompanyTree(dept, parentDept, companyByOaId)) {
|
||||
return parentDept;
|
||||
}
|
||||
return companyByOaId.get(parentOaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按所属公司编码或 OA 主键定位本地公司
|
||||
*/
|
||||
private Dept findBelongCompany(Dept dept, Map<String, Dept> companyByOaId) {
|
||||
if (dept == null || companyByOaId == null || companyByOaId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtil.isNotBlank(dept.getBelongCompanyCode())) {
|
||||
for (Dept company : companyByOaId.values()) {
|
||||
if (company != null && StringUtil.equals(company.getDeptCode(), dept.getBelongCompanyCode())) {
|
||||
return company;
|
||||
}
|
||||
}
|
||||
Dept byOaId = companyByOaId.get(dept.getBelongCompanyCode());
|
||||
if (byOaId != null) {
|
||||
return byOaId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个组织是否同属一家 OA 公司(用于部门 oaId 与公司 oaId 撞号消歧)
|
||||
*/
|
||||
private boolean isSameOaCompanyTree(Dept left, Dept right, Map<String, Dept> companyByOaId) {
|
||||
if (left == null || right == null) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtil.isNotBlank(left.getBelongCompanyCode()) && StringUtil.isNotBlank(right.getBelongCompanyCode())) {
|
||||
return StringUtil.equals(left.getBelongCompanyCode(), right.getBelongCompanyCode());
|
||||
}
|
||||
Dept leftCompany = this.findBelongCompany(left, companyByOaId);
|
||||
Dept rightCompany = this.isOaCompany(right) ? right : this.findBelongCompany(right, companyByOaId);
|
||||
if (leftCompany == null || rightCompany == null || leftCompany.getId() == null || rightCompany.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(leftCompany.getId(), rightCompany.getId());
|
||||
}
|
||||
|
||||
private String resolveParentCode(Dept parent, String prefix, String parentOaId) {
|
||||
if (StringUtil.isNotBlank(parent.getDeptCode())) {
|
||||
return parent.getDeptCode();
|
||||
|
||||
+111
-53
@@ -153,10 +153,10 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
public OaOrgSyncPageVO syncDepartmentPage(int current, int size, boolean incremental, String modifiedSince, String companyOaId) {
|
||||
try {
|
||||
ComposeLogUtil.addLog(log);
|
||||
return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince);
|
||||
return this.syncDepartmentFromOaPage(current, size, incremental, modifiedSince, companyOaId);
|
||||
} finally {
|
||||
ComposeLogUtil.removeLastLog();
|
||||
}
|
||||
@@ -224,29 +224,28 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步部门
|
||||
* 同步部门:先整理已同步公司,再按公司 oaId 逐个轮询查询部门
|
||||
* @param startTime 查询开始时间
|
||||
*/
|
||||
private void syncDept(Date startTime) {
|
||||
String subCompanyIds = getOaSubCompanyIds(false);
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
List<String> companyOaIds = listAllowedCompanyOaIds(false);
|
||||
if (CollectionUtil.isEmpty(companyOaIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到子公司查询参数");
|
||||
return;
|
||||
}
|
||||
// 未处理的数据
|
||||
List<Dept> notHandleList = new ArrayList<>();
|
||||
// 1. 设置查询参数
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, subCompanyIds);
|
||||
// 2. 分页查询并处理数据
|
||||
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询部门信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||
// 处理数据
|
||||
OrgSyncCount syncCount = handleDept(list, false);
|
||||
notHandleList.addAll(syncCount.getNotHandledList());
|
||||
});
|
||||
// 3. 未处理的数据
|
||||
for (String companyOaId : companyOaIds) {
|
||||
ComposeLogUtil.getLastLog().info("按公司轮询同步部门 companyOaId={}", companyOaId);
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(startTime, companyOaId);
|
||||
OAUtils.pageSyncHandler(departmentSearch, param -> oaClient.queryDepartmentPage(new OASearch<>(param)), response -> {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 companyOaId={} {}", companyOaId, JSON.toJSONString(response));
|
||||
return new ServiceException("调用OA接口查询部门信息失败");
|
||||
}, 10000, ComposeLogUtil.getLastLog()::info).accept(list -> {
|
||||
OrgSyncCount syncCount = handleDept(list, false);
|
||||
notHandleList.addAll(syncCount.getNotHandledList());
|
||||
});
|
||||
}
|
||||
if (CollectionUtil.isNotEmpty(notHandleList)) {
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,还有未处理的数据:{}", JSON.toJSONString(notHandleList));
|
||||
}
|
||||
@@ -398,34 +397,55 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按页从 OA 部门接口同步部门
|
||||
* 按页从 OA 部门接口同步部门:公司数据整理完成后,按公司 oaId 逐个轮询查询
|
||||
*
|
||||
* @param current 当前页
|
||||
* @param current 当前页(相对当前公司)
|
||||
* @param size 每页条数
|
||||
* @param incremental 是否增量同步
|
||||
* @param modifiedSince 增量起始时间(后续分页回传)
|
||||
* @param companyOaId 当前公司 OA 主键;空则从第一家开始
|
||||
* @return 本页同步结果
|
||||
*/
|
||||
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince) {
|
||||
private OaOrgSyncPageVO syncDepartmentFromOaPage(int current, int size, boolean incremental, String modifiedSince,
|
||||
String companyOaId) {
|
||||
int pageNo = current < 1 ? 1 : current;
|
||||
int pageSize = size < 1 ? 20 : Math.min(size, 200);
|
||||
String subCompanyIds = getOaSubCompanyIds(true);
|
||||
if (StringUtils.isEmpty(subCompanyIds)) {
|
||||
// 先整理已同步企业列表,再按企业轮询
|
||||
List<String> companyOaIds = listAllowedCompanyOaIds(true);
|
||||
if (CollectionUtil.isEmpty(companyOaIds)) {
|
||||
ComposeLogUtil.getLastLog().warn("未查询到已同步的OA公司,跳过部门同步");
|
||||
OaOrgSyncPageVO emptyPageVO = buildOrgSyncPageVO("department", pageNo, pageSize, 0L, 0, OrgSyncCount.empty());
|
||||
emptyPageVO.setFinished(true);
|
||||
emptyPageVO.setCompanyFinished(true);
|
||||
emptyPageVO.setCompanyIndex(0);
|
||||
emptyPageVO.setCompanyTotal(0);
|
||||
if (!incremental) {
|
||||
emptyPageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
}
|
||||
return emptyPageVO;
|
||||
}
|
||||
int companyIndex = 0;
|
||||
String currentCompanyOaId = companyOaIds.get(0);
|
||||
if (StringUtils.isNotBlank(companyOaId)) {
|
||||
int foundIndex = companyOaIds.indexOf(companyOaId.trim());
|
||||
if (foundIndex >= 0) {
|
||||
companyIndex = foundIndex;
|
||||
currentCompanyOaId = companyOaIds.get(foundIndex);
|
||||
} else {
|
||||
ComposeLogUtil.getLastLog().warn("回传公司oaId不在待同步列表中,回退第一家 companyOaId={}", companyOaId);
|
||||
}
|
||||
}
|
||||
IncrementalStart incrementalStart = resolveIncrementalStart(incremental, modifiedSince, DeptCategory.DEPT);
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), subCompanyIds);
|
||||
// 查询部门时只传当前公司的 oa_id
|
||||
OADepartmentSearch departmentSearch = buildDepartmentSearch(incrementalStart.getStartTime(), currentCompanyOaId);
|
||||
departmentSearch.setCurPage(pageNo);
|
||||
departmentSearch.setPageSize(pageSize);
|
||||
ComposeLogUtil.getLastLog().info("按公司查询OA部门 companyOaId={} companyIndex={}/{} page={}/{}",
|
||||
currentCompanyOaId, companyIndex + 1, companyOaIds.size(), pageNo, pageSize);
|
||||
OAResponse<OADepartmentResponse> oaResponse = oaClient.queryDepartmentPage(new OASearch<>(departmentSearch));
|
||||
if (oaResponse == null || !OAConstant.OA_SUCCESS_CODE.equals(oaResponse.getCode()) || oaResponse.getData() == null) {
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 {}", JSON.toJSONString(oaResponse));
|
||||
ComposeLogUtil.getLastLog().error("调用OA接口查询部门信息失败 companyOaId={} {}",
|
||||
currentCompanyOaId, JSON.toJSONString(oaResponse));
|
||||
throw new ServiceException("调用OA接口查询部门信息失败");
|
||||
}
|
||||
OAResponseData<OADepartmentResponse> responseData = oaResponse.getData();
|
||||
@@ -437,25 +457,45 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
OrgSyncCount syncCount = handleDept(oaDepartments, true);
|
||||
if (CollectionUtil.isNotEmpty(syncCount.getNotHandledList())) {
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据:{}", JSON.toJSONString(syncCount.getNotHandledList()));
|
||||
ComposeLogUtil.getLastLog().warn("同步部门,本页未处理数据 companyOaId={}:{}",
|
||||
currentCompanyOaId, JSON.toJSONString(syncCount.getNotHandledList()));
|
||||
}
|
||||
OaOrgSyncPageVO pageVO = buildOrgSyncPageVO("department", pageNo, pageSize, totalSize, oaDepartments.size(), syncCount);
|
||||
pageVO.setModifiedSince(incrementalStart.getModifiedSince());
|
||||
if (Boolean.TRUE.equals(pageVO.getFinished())) {
|
||||
// 部门分批同步完成后,清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
deptService.updateAncestors(null);
|
||||
if (!incremental) {
|
||||
pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
pageVO.setCompanyOaId(currentCompanyOaId);
|
||||
pageVO.setCompanyIndex(companyIndex + 1);
|
||||
pageVO.setCompanyTotal(companyOaIds.size());
|
||||
boolean companyPageFinished = Boolean.TRUE.equals(pageVO.getFinished());
|
||||
if (companyPageFinished) {
|
||||
if (companyIndex + 1 < companyOaIds.size()) {
|
||||
// 当前公司部门已拉完:先按 OA 主键校正父子挂载,再进入下一公司
|
||||
pageVO.setCompanyFinished(true);
|
||||
pageVO.setNextCompanyOaId(companyOaIds.get(companyIndex + 1));
|
||||
pageVO.setFinished(false);
|
||||
deptService.rebuildOaHierarchy();
|
||||
} else {
|
||||
// 全部公司轮询完成:清理白名单外组织、重建层级、清理无法挂载数据并刷新祖级列表
|
||||
pageVO.setCompanyFinished(true);
|
||||
pageVO.setNextCompanyOaId(null);
|
||||
pageVO.setFinished(true);
|
||||
this.removeOutsideWhitelistOrgs();
|
||||
deptService.rebuildOaHierarchy();
|
||||
deptService.removeOaOrphansWithoutParent();
|
||||
deptService.updateAncestors(null);
|
||||
if (!incremental) {
|
||||
pageVO.setDumpFilePath(oaSyncRawDumpHelper.finishOrgFullDump());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pageVO.setCompanyFinished(false);
|
||||
pageVO.setNextCompanyOaId(currentCompanyOaId);
|
||||
pageVO.setFinished(false);
|
||||
}
|
||||
CacheUtil.clear(SYS_CACHE);
|
||||
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
|
||||
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} {}/{},成功{},跳过{}",
|
||||
incremental ? "incremental" : "full", pageNo, totalSize,
|
||||
syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||
ComposeLogUtil.getLastLog().info("OA部门分页同步完成 mode={} companyOaId={} company={}/{} page={}/{},成功{},跳过{}",
|
||||
incremental ? "incremental" : "full", currentCompanyOaId, companyIndex + 1, companyOaIds.size(),
|
||||
pageNo, totalSize, syncCount.getSyncedCount(), syncCount.getSkippedCount());
|
||||
return pageVO;
|
||||
}
|
||||
|
||||
@@ -604,15 +644,15 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
/**
|
||||
* 组装 OA 部门分页查询参数
|
||||
*
|
||||
* @param startTime 增量查询开始时间
|
||||
* @param subCompanyIds 子公司 id 列表
|
||||
* @param startTime 增量查询开始时间
|
||||
* @param companyOaId 当前公司 OA 主键(单次只传一家公司)
|
||||
* @return 查询参数
|
||||
*/
|
||||
private OADepartmentSearch buildDepartmentSearch(Date startTime, String subCompanyIds) {
|
||||
private OADepartmentSearch buildDepartmentSearch(Date startTime, String companyOaId) {
|
||||
OADepartmentSearch departmentSearch = new OADepartmentSearch();
|
||||
departmentSearch.setCurPage(1);
|
||||
departmentSearch.setPageSize(20);
|
||||
departmentSearch.setSubcompanyid1(subCompanyIds);
|
||||
departmentSearch.setSubcompanyid1(companyOaId);
|
||||
if (startTime != null) {
|
||||
departmentSearch.setModified(DateUtil.format(startTime, DateUtil.PATTERN_DATETIME));
|
||||
}
|
||||
@@ -766,9 +806,11 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
String parentOaId = dept.getOaSupSubComId();
|
||||
Dept belongCompany = companyByOaId.values().stream()
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode()))
|
||||
.filter(company -> StringUtils.equals(company.getDeptCode(), dept.getBelongCompanyCode())
|
||||
|| StringUtils.equals(company.getOaId(), dept.getBelongCompanyCode()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
// 根部门:oaSupSubComId 即所属公司 OA 主键,必须挂公司(OA 公司/部门主键会撞号)
|
||||
if (belongCompany != null && (OAConstant.ROOT_COMPANY_ID.equals(parentOaId)
|
||||
|| StringUtils.equals(parentOaId, belongCompany.getOaId()))) {
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
@@ -779,9 +821,19 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
if (parentDept == null) {
|
||||
parentDept = existingByOaId.get(parentOaId);
|
||||
}
|
||||
// 跨公司同号部门不可作为上级(例如总部部门 id=19 与公司 id=19)
|
||||
if (parentDept != null && StringUtils.isNotBlank(dept.getBelongCompanyCode())
|
||||
&& StringUtils.isNotBlank(parentDept.getBelongCompanyCode())
|
||||
&& !StringUtils.equals(dept.getBelongCompanyCode(), parentDept.getBelongCompanyCode())) {
|
||||
parentDept = null;
|
||||
}
|
||||
Dept parentCompany = StringUtils.isNotBlank(parentOaId) ? companyByOaId.get(parentOaId) : null;
|
||||
if (parentDept != null && StringUtils.isNotBlank(parentDept.getDeptCode())) {
|
||||
dept.setParentCode(parentDept.getDeptCode());
|
||||
dept.setParentId(parentDept.getId());
|
||||
} else if (parentCompany != null && StringUtils.isNotBlank(parentCompany.getDeptCode())) {
|
||||
dept.setParentCode(parentCompany.getDeptCode());
|
||||
dept.setParentId(parentCompany.getId());
|
||||
} else if (belongCompany != null) {
|
||||
// 上级部门尚未就绪时先挂所属公司,分批结束后按 OA 主键重建
|
||||
dept.setParentCode(belongCompany.getDeptCode());
|
||||
@@ -995,7 +1047,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
|
||||
/**
|
||||
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
|
||||
* 组织编码已按新规则重算且与库中不一致时,即使 modified 相同也要更新。
|
||||
* 组织编码已按新规则重算、或父子挂载与库中不一致时,即使 modified 相同也要更新。
|
||||
*/
|
||||
private boolean shouldSkipOaUpdate(Dept existing, Dept incoming) {
|
||||
if (existing == null) {
|
||||
@@ -1005,6 +1057,15 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
&& !StringUtils.equals(existing.getDeptCode(), incoming.getDeptCode())) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && incoming.getParentId() != null
|
||||
&& !Objects.equals(existing.getParentId(), incoming.getParentId())) {
|
||||
return false;
|
||||
}
|
||||
if (incoming != null && StringUtils.isNotBlank(incoming.getParentCode())
|
||||
&& !StringUtils.equals(StringUtils.trimToEmpty(existing.getParentCode()),
|
||||
StringUtils.trimToEmpty(incoming.getParentCode()))) {
|
||||
return false;
|
||||
}
|
||||
String localModified = StringUtils.trimToEmpty(existing.getOaModified());
|
||||
String remoteModified = StringUtils.trimToEmpty(incoming == null ? null : incoming.getOaModified());
|
||||
if (localModified.isEmpty() || remoteModified.isEmpty()) {
|
||||
@@ -1145,12 +1206,12 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OA 部门查询使用的子公司 id
|
||||
* 整理部门同步用的公司 OA 主键列表(公司阶段完成后调用)
|
||||
*
|
||||
* @param syncAll true=取已同步的全部 OA 公司;false=仅取字典配置的公司
|
||||
* @return 逗号分隔的 OA 公司 id
|
||||
* @return 白名单内公司 oaId 列表(去重、保序)
|
||||
*/
|
||||
private String getOaSubCompanyIds(boolean syncAll) {
|
||||
private List<String> listAllowedCompanyOaIds(boolean syncAll) {
|
||||
List<Dept> companyList;
|
||||
if (syncAll) {
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
@@ -1161,7 +1222,7 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
} else {
|
||||
Set<String> companyNames = this.getCompanyNames();
|
||||
if (companyNames.isEmpty()) {
|
||||
return null;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
companyList = deptService.list(Wrappers.<Dept>lambdaQuery()
|
||||
.in(Dept::getDeptName, companyNames)
|
||||
@@ -1170,20 +1231,17 @@ public class OASyncServiceImpl implements IOASyncService {
|
||||
.ne(Dept::getOaId, "")
|
||||
);
|
||||
}
|
||||
Map<String, Dept> companyByOaId = new HashMap<>();
|
||||
Map<String, Dept> companyByOaId = new LinkedHashMap<>();
|
||||
for (Dept company : companyList) {
|
||||
String oaId = this.resolveOaCompanyId(company);
|
||||
if (StringUtils.isNotBlank(oaId)) {
|
||||
companyByOaId.put(oaId, company);
|
||||
companyByOaId.putIfAbsent(oaId, company);
|
||||
}
|
||||
}
|
||||
Set<String> allowedCompanyOaIds = resolveAllowedCompanyOaIds(companyByOaId);
|
||||
return companyList.stream()
|
||||
.map(this::resolveOaCompanyId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
return companyByOaId.keySet().stream()
|
||||
.filter(allowedCompanyOaIds::contains)
|
||||
.distinct()
|
||||
.collect(Collectors.joining(","));
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+33
-8
@@ -1,6 +1,7 @@
|
||||
package org.springblade.transport.mk;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
|
||||
@@ -270,6 +271,7 @@ public final class MkProcessBizHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
@@ -290,6 +292,12 @@ public final class MkProcessBizHandlers {
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
FormalSettlement entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
log.warn("MK同步当前节点失败,正式结算单不存在 id={}", id);
|
||||
return false;
|
||||
}
|
||||
// 已终态单据禁止再写回审批中,避免流程结束后的节点刷新把状态打回 reviewing
|
||||
if ("approved".equals(entity.getApprovalStatus()) || "returned".equals(entity.getApprovalStatus())
|
||||
|| "voided".equals(entity.getApprovalStatus())) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
@@ -310,8 +318,12 @@ public final class MkProcessBizHandlers {
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
FormalSettlement entity = service.getById(id);
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
if (entity == null) {
|
||||
log.error("MK审批通过同步失败,正式结算单不存在 id={}", id);
|
||||
return false;
|
||||
}
|
||||
if ("approved".equals(entity.getApprovalStatus())) {
|
||||
return true;
|
||||
}
|
||||
// 统一走 service.approve:与手动审批同一入口(补变更记录),审批事务提交后自动触发金蝶推送事件;
|
||||
// 仅审批中单据允许通过,重复回调被状态校验拒绝,不重复推送
|
||||
@@ -330,8 +342,12 @@ public final class MkProcessBizHandlers {
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
FormalSettlement entity = service.getById(id);
|
||||
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
if (entity == null) {
|
||||
log.error("MK审批驳回同步失败,正式结算单不存在 id={}", id);
|
||||
return false;
|
||||
}
|
||||
if ("returned".equals(entity.getApprovalStatus())) {
|
||||
return true;
|
||||
}
|
||||
entity.setApprovalStatus("returned");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
@@ -403,10 +419,19 @@ public final class MkProcessBizHandlers {
|
||||
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("returned");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
if ("reviewing".equals(entity.getApprovalStatus())) {
|
||||
PaymentApplicationStatusRequest request = new PaymentApplicationStatusRequest();
|
||||
request.setId(id);
|
||||
service.returnBill(request);
|
||||
}
|
||||
PaymentApplication updated = service.getById(id);
|
||||
if (updated == null) {
|
||||
return false;
|
||||
}
|
||||
updated.setApprovalStatus("returned");
|
||||
updated.setCurrentNode("审核不通过");
|
||||
updated.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(updated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+75
-28
@@ -4,10 +4,12 @@ import com.alibaba.fastjson2.JSON;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tenant.TenantUtil;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.process.feign.IBusinessProcessClient;
|
||||
import org.springblade.process.pojo.vo.MkFormBindVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -69,9 +71,9 @@ public class MkProcessMessageService {
|
||||
log.warn("公开页流程消息未找到 processId,跳过查询当前节点 bizType={}", bizType);
|
||||
return;
|
||||
}
|
||||
String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId");
|
||||
Long bizId = resolveBizId(asMap(body == null ? null : body.get("formData")));
|
||||
queryCurrentNodes(bizType, processId, loginName);
|
||||
scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues);
|
||||
scheduleProcessInfo(handler, bizType, processId, loginName, bizId, formValues);
|
||||
}
|
||||
|
||||
private void queryCurrentNodes(String bizType, String processId, String loginName) {
|
||||
@@ -86,36 +88,35 @@ public class MkProcessMessageService {
|
||||
}
|
||||
|
||||
private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
|
||||
String loginName, String bizIdText, Map<String, Object> formValues) {
|
||||
String loginName, Long bizId, Map<String, Object> formValues) {
|
||||
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
|
||||
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId);
|
||||
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={} bizId={}",
|
||||
PROCESS_INFO_DELAY_SECONDS, bizType, processId, bizId);
|
||||
// 延迟任务跑在独立线程,请求态 @TenantIgnore 已丢失,必须显式 ignore,否则 getById 可能因租户条件查空
|
||||
scheduler.schedule(
|
||||
() -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy),
|
||||
() -> TenantUtil.ignore(() -> queryProcessInfo(handler, bizType, processId, loginName, bizId, formValuesCopy)),
|
||||
PROCESS_INFO_DELAY_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
|
||||
String loginName, String bizIdText, Map<String, Object> formValues) {
|
||||
String loginName, Long bizId, Map<String, Object> formValues) {
|
||||
try {
|
||||
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
|
||||
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
|
||||
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}",
|
||||
bizType, processId, loginName, JSON.toJSONString(processInfo));
|
||||
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} bizId={} result={}",
|
||||
bizType, processId, loginName, bizId, JSON.toJSONString(processInfo));
|
||||
if (isProcessFinished(processInfo)) {
|
||||
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
|
||||
applyProcessResult(handler, bizType, bizId, processId, formValues, true);
|
||||
} else if (MkProcessNodeHelper.isProcessRejected(processInfo)) {
|
||||
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
|
||||
} else if (StringUtil.isNotBlank(bizIdText)) {
|
||||
try {
|
||||
handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText);
|
||||
}
|
||||
applyProcessResult(handler, bizType, bizId, processId, formValues, false);
|
||||
} else if (bizId != null) {
|
||||
handler.syncFromProcessInfo(bizId, processInfo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
|
||||
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={} bizId={}",
|
||||
bizType, processId, loginName, bizId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,21 +124,24 @@ public class MkProcessMessageService {
|
||||
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
|
||||
}
|
||||
|
||||
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
|
||||
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, Long bizId,
|
||||
String processId, Map<String, Object> formValues, boolean approved) {
|
||||
String action = approved ? "审核通过" : "审核驳回";
|
||||
if (StringUtil.isBlank(bizIdText)) {
|
||||
log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId);
|
||||
if (bizId == null) {
|
||||
log.warn("流程{}但未解析到业务主键,跳过同步 bizType={} processId={}", action, bizType, processId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
|
||||
Long bizId = Long.valueOf(bizIdText);
|
||||
if (approved) {
|
||||
handler.approveFromProcess(bizId, processorName);
|
||||
} else {
|
||||
handler.rejectFromProcess(bizId, processorName);
|
||||
boolean synced = approved
|
||||
? handler.approveFromProcess(bizId, processorName)
|
||||
: handler.rejectFromProcess(bizId, processorName);
|
||||
if (!synced) {
|
||||
log.error("流程{}同步业务状态失败(业务单据不存在或未更新) bizType={} bizId={} processId={}",
|
||||
action, bizType, bizId, processId);
|
||||
return;
|
||||
}
|
||||
log.info("流程{}同步业务状态成功 bizType={} bizId={} processId={}", action, bizType, bizId, processId);
|
||||
try {
|
||||
FR<Boolean> markResult = businessProcessClient.markProcessFinished(processId, approved);
|
||||
log.info("公开页标记业务流程完结 bizType={} processId={} approved={} result={}",
|
||||
@@ -146,13 +150,56 @@ public class MkProcessMessageService {
|
||||
log.error("公开页标记业务流程完结失败 bizType={} processId={} approved={}",
|
||||
bizType, processId, approved, markException);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
|
||||
} catch (Exception e) {
|
||||
log.error("流程{}同步业务状态失败 bizType={} id={} processId={}", action, bizType, bizIdText, processId, e);
|
||||
log.error("流程{}同步业务状态失败 bizType={} bizId={} processId={}", action, bizType, bizId, processId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开页 formData 里的 id / formInstanceId 可能是表单实例 id(与业务主键不同),
|
||||
* 须先按 MkFormBind 反查业务主键,与 detail 接口保持一致。
|
||||
*/
|
||||
private Long resolveBizId(Map<String, Object> formData) {
|
||||
String formInstanceIdText = firstText(formData, "formInstanceId");
|
||||
String idText = firstText(formData, "id");
|
||||
Long resolved = resolveByFormInstanceId(formInstanceIdText);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
resolved = resolveByFormInstanceId(idText);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
if (StringUtil.isNotBlank(idText)) {
|
||||
try {
|
||||
return Long.valueOf(idText);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("公开页流程消息业务id格式不正确 id={}", idText);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long resolveByFormInstanceId(String formInstanceIdText) {
|
||||
if (StringUtil.isBlank(formInstanceIdText)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Long formInstanceId = Long.valueOf(formInstanceIdText);
|
||||
FR<MkFormBindVO> result = businessProcessClient.resolveByFormInstanceId(formInstanceId);
|
||||
if (result != null && result.isSuccess() && result.getData() != null && result.getData().getBizId() != null) {
|
||||
Long bizId = result.getData().getBizId();
|
||||
log.info("公开页按表单实例id解析业务主键 formInstanceId={} bizId={}", formInstanceId, bizId);
|
||||
return bizId;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("公开页表单实例id格式不正确 formInstanceId={}", formInstanceIdText);
|
||||
} catch (Exception e) {
|
||||
log.warn("公开页按表单实例id解析业务主键失败 formInstanceId={}", formInstanceIdText, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, Object> asMap(Object value) {
|
||||
if (!(value instanceof Map<?, ?> map)) {
|
||||
return Collections.emptyMap();
|
||||
|
||||
+6
-1
@@ -104,12 +104,17 @@ public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, Bil
|
||||
}
|
||||
|
||||
private List<BillLedgerVO> availableList(String keyword, Long deptId, Long selectedId) {
|
||||
LocalDate today = LocalDate.now();
|
||||
// 付款申请仅可选:收票类型、未到期、有可用余额;已选中的汇票始终带回以便回显
|
||||
return list(Wrappers.<BillLedger>lambdaQuery()
|
||||
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword)
|
||||
.or().like(BillLedger::getIssuerName, keyword)
|
||||
.or().like(BillLedger::getReceiverName, keyword))
|
||||
.and(wrapper -> wrapper
|
||||
.gt(BillLedger::getAvailableBalance, BigDecimal.ZERO)
|
||||
.and(available -> available
|
||||
.eq(BillLedger::getBillType, "received")
|
||||
.ge(BillLedger::getMaturityDate, today)
|
||||
.gt(BillLedger::getAvailableBalance, BigDecimal.ZERO))
|
||||
.or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId)))
|
||||
.orderByAsc(BillLedger::getMaturityDate)
|
||||
.orderByDesc(BillLedger::getCreateTime)).stream()
|
||||
|
||||
+286
-9
@@ -329,6 +329,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, id).orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
fillDetailWaybillAddresses(details);
|
||||
fillDetailCargoNames(details);
|
||||
fillDetailBillingTypes(details);
|
||||
vo.setDetails(details);
|
||||
vo.setSummaryFees(listSummaryFees(id));
|
||||
vo.setPayments(paymentMapper.selectList(Wrappers.<FormalSettlementPayment>lambdaQuery()
|
||||
@@ -367,6 +369,36 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 明细展示计费方式:按来源应收应付货物费用的计费类型汇总(去重后顿号拼接)。
|
||||
*/
|
||||
private void fillDetailBillingTypes(List<FormalSettlementDetail> details) {
|
||||
if (details == null || details.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> sourceDetailIds = details.stream()
|
||||
.map(FormalSettlementDetail::getSourceDetailId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (sourceDetailIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> billingTypeMap = receivablePayableCargoFeeMapper.selectList(
|
||||
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.in(ReceivablePayableCargoFee::getDetailId, sourceDetailIds)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
|
||||
.stream()
|
||||
.filter(fee -> Func.isNotEmpty(fee.getBillingType()))
|
||||
.collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId,
|
||||
Collectors.mapping(ReceivablePayableCargoFee::getBillingType,
|
||||
Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new),
|
||||
values -> String.join("、", values)))));
|
||||
for (FormalSettlementDetail detail : details) {
|
||||
detail.setBillingType(billingTypeMap.getOrDefault(detail.getSourceDetailId(), ""));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为历史正式结算明细补充运单收发货信息,保留明细中已有的快照值。
|
||||
*/
|
||||
@@ -1239,9 +1271,23 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
public List<FormalSettlementDetailFee> detailFees(Long detailId) {
|
||||
FormalSettlementDetail detail = detailMapper.selectById(detailId);
|
||||
if (detail == null || Objects.equals(detail.getIsDeleted(), 1)) throw new ServiceException("正式结算明细不存在");
|
||||
return detailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
List<FormalSettlementDetailFee> fees = detailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detailId)
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo));
|
||||
List<Long> sourceFeeIds = fees.stream().map(FormalSettlementDetailFee::getSourceFeeId)
|
||||
.filter(Objects::nonNull).toList();
|
||||
if (sourceFeeIds.isEmpty()) {
|
||||
return fees;
|
||||
}
|
||||
Map<Long, ReceivablePayableCargoFee> sourceFeeMap = receivablePayableCargoFeeMapper.selectBatchIds(sourceFeeIds).stream()
|
||||
.collect(Collectors.toMap(ReceivablePayableCargoFee::getId, Function.identity(), (left, right) -> left));
|
||||
fees.forEach(fee -> {
|
||||
ReceivablePayableCargoFee sourceFee = sourceFeeMap.get(fee.getSourceFeeId());
|
||||
if (sourceFee != null) {
|
||||
fee.setBillingRulesJson(sourceFee.getBillingRulesJson());
|
||||
}
|
||||
});
|
||||
return fees;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1251,11 +1297,15 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
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), "调整原因");
|
||||
// 弹窗底部备注非必填,写入结算明细备注
|
||||
String detailRemark = limit(request.getChangeReason(), 200);
|
||||
String beforeDetailRemark = detail.getRemark() == null ? "" : detail.getRemark();
|
||||
Map<Long, FormalSettlementDetailFee> existing = detailFees(detail.getId()).stream()
|
||||
.collect(Collectors.toMap(FormalSettlementDetailFee::getId, item -> item));
|
||||
if (existing.size() != request.getRows().size()) throw new ServiceException("费用调整行数据不完整");
|
||||
List<String> changes = new java.util.ArrayList<>();
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
for (PreSettlementDetailAdjustRequest.FeeRow row : request.getRows()) {
|
||||
FormalSettlementDetailFee fee = existing.get(row.getId());
|
||||
if (fee == null) throw new ServiceException("存在无效的货物费用行");
|
||||
@@ -1268,7 +1318,6 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
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);
|
||||
if (beforeAmount.compareTo(fee.getSettlementAmountTax()) != 0) {
|
||||
changes.add("【" + firstNotEmpty(fee.getCargoName(), fee.getLineNo())
|
||||
@@ -1278,17 +1327,31 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
List<FormalSettlementDetailFee> rows = detailFees(detail.getId());
|
||||
detail.setTransportQuantity(rows.stream().map(FormalSettlementDetailFee::getTransportQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
detail.setMileage(rows.stream().map(FormalSettlementDetailFee::getMileage).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
// 运输单价无法按行简单求和,与前端暂存逻辑一致:取首行费用单价。
|
||||
detail.setUnitPrice(rows.stream().map(FormalSettlementDetailFee::getUnitPrice).filter(Objects::nonNull).findFirst().orElse(null));
|
||||
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));
|
||||
boolean hasNoTax = rows.stream().anyMatch(fee -> fee.getSettlementAmountNoTax() != null);
|
||||
detail.setSettlementAmountNoTax(hasNoTax
|
||||
? rows.stream().map(FormalSettlementDetailFee::getSettlementAmountNoTax).filter(Objects::nonNull).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
: null);
|
||||
detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(detail.getOriginalAmount()));
|
||||
detail.setRemark(reason);
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
rows.forEach(fee -> parseFeeItems(fee.getFeeItemsJson()).forEach((name, amount) ->
|
||||
feeItems.merge(name, money(amount), BigDecimal::add)));
|
||||
detail.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
detail.setRemark(detailRemark);
|
||||
detailMapper.updateById(detail);
|
||||
appendTextChange(changes, beforeData, afterData, "备注", beforeDetailRemark, detailRemark);
|
||||
if (changes.isEmpty()) {
|
||||
throw new ServiceException("未修改任何结算明细费用");
|
||||
}
|
||||
rebuildSummaryFees(settlement.getId());
|
||||
refreshSettlementAmount(settlement);
|
||||
saveChange(settlement.getId(), "结算明细项", detail.getLineNo(), "调整",
|
||||
changes.isEmpty() ? "调整结算明细费用" : String.join(";", changes), reason);
|
||||
String.join(";", changes), detailRemark, beforeData, afterData);
|
||||
}
|
||||
|
||||
private void rebuildSummaryFees(Long settlementId) {
|
||||
@@ -1463,10 +1526,18 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
// throw new ServiceException("费用类型与费用项不匹配或费用项已停用");
|
||||
// }
|
||||
FormalSettlementSummaryFee row = requestRow.getId() == null ? null : existingMap.get(requestRow.getId());
|
||||
if (row == null && requestRow.getId() == null) row = new FormalSettlementSummaryFee();
|
||||
if (row == null && requestRow.getId() == null) {
|
||||
row = new FormalSettlementSummaryFee();
|
||||
}
|
||||
if (row == null || row.getId() != null && !Integer.valueOf(1).equals(row.getManualFlag())) {
|
||||
throw new ServiceException("存在无效的手工费用行");
|
||||
}
|
||||
boolean creating = row.getId() == null;
|
||||
String beforeFeeType = creating ? null : row.getFeeType();
|
||||
String beforeFeeItem = creating ? null : row.getFeeItem();
|
||||
BigDecimal beforeAdjustAmount = creating ? null : money(row.getAdjustAmount());
|
||||
BigDecimal beforeSettlementAmount = creating ? null : money(row.getSettlementAmount());
|
||||
String beforeRemark = creating ? null : row.getRemark();
|
||||
row.setFormalSettlementId(settlementId);
|
||||
row.setFeeType(feeType);
|
||||
row.setFeeItem(feeItem);
|
||||
@@ -1475,13 +1546,15 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
row.setSettlementAmount(row.getAdjustAmount());
|
||||
row.setRemark(limit(requestRow.getRemark(), 50));
|
||||
row.setManualFlag(1);
|
||||
if (row.getId() == null) {
|
||||
if (creating) {
|
||||
row.setLineNo(nextLineNo++);
|
||||
summaryFeeMapper.insert(row);
|
||||
} else {
|
||||
summaryFeeMapper.updateById(row);
|
||||
}
|
||||
retainedManualIds.add(row.getId());
|
||||
saveManualSummaryFeeChange(settlementId, creating, beforeFeeType, beforeFeeItem,
|
||||
beforeAdjustAmount, beforeSettlementAmount, beforeRemark, row);
|
||||
continue;
|
||||
}
|
||||
FormalSettlementSummaryFee row = existingMap.get(requestRow.getId());
|
||||
@@ -1495,13 +1568,43 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
if (row == null || Integer.valueOf(1).equals(row.getManualFlag())) {
|
||||
throw new ServiceException("存在无效的结算合计行");
|
||||
}
|
||||
BigDecimal beforeAdjustAmount = money(row.getAdjustAmount());
|
||||
BigDecimal beforeSettlementAmount = money(row.getSettlementAmount());
|
||||
String beforeRemark = row.getRemark();
|
||||
row.setAdjustAmount(money(requestRow.getAdjustAmount()));
|
||||
row.setSettlementAmount(money(row.getOriginalAmount()).add(row.getAdjustAmount()));
|
||||
row.setRemark(limit(requestRow.getRemark(), 50));
|
||||
summaryFeeMapper.updateById(row);
|
||||
List<String> changes = new java.util.ArrayList<>();
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount());
|
||||
appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount());
|
||||
appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark());
|
||||
if (!changes.isEmpty()) {
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
}
|
||||
}
|
||||
for (FormalSettlementSummaryFee manualRow : existingManualRows) {
|
||||
if (!retainedManualIds.contains(manualRow.getId())) summaryFeeMapper.deleteById(manualRow.getId());
|
||||
if (!retainedManualIds.contains(manualRow.getId())) {
|
||||
summaryFeeMapper.deleteById(manualRow.getId());
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
beforeData.put("费用类型", manualRow.getFeeType());
|
||||
beforeData.put("费用项", manualRow.getFeeItem());
|
||||
beforeData.put("调整金额", money(manualRow.getAdjustAmount()));
|
||||
beforeData.put("结算金额", money(manualRow.getSettlementAmount()));
|
||||
beforeData.put("备注", manualRow.getRemark());
|
||||
afterData.put("费用类型", "");
|
||||
afterData.put("费用项", "");
|
||||
afterData.put("调整金额", BigDecimal.ZERO.setScale(2));
|
||||
afterData.put("结算金额", BigDecimal.ZERO.setScale(2));
|
||||
afterData.put("备注", "");
|
||||
saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除",
|
||||
"删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】",
|
||||
"", beforeData, afterData);
|
||||
}
|
||||
}
|
||||
renumberSummaryFees(settlementId);
|
||||
}
|
||||
@@ -1819,6 +1922,117 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
.set(ReceivablePayableDetail::getSettlementStatus, "formal_settled"));
|
||||
if (affected != 1) throw new ServiceException("单据" + source.getDocumentNo() + "已被其他结算单选择");
|
||||
}
|
||||
persistDetailCargoNames(settlement.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 一单多货时运单主表只保留首行货物。结算明细按运单货物清单回填全部货物名称、货物类型。
|
||||
*/
|
||||
private void fillDetailCargoNames(List<FormalSettlementDetail> details) {
|
||||
if (details == null || details.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> waybillIds = details.stream().map(FormalSettlementDetail::getWaybillId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, Waybill> waybillMap = waybillIds.isEmpty() ? Map.of() : waybillService.listByIds(waybillIds).stream()
|
||||
.collect(Collectors.toMap(Waybill::getId, Function.identity(), (left, right) -> left));
|
||||
List<Long> detailIds = details.stream().map(FormalSettlementDetail::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, List<FormalSettlementDetailFee>> feesByDetail = detailIds.isEmpty() ? Map.of()
|
||||
: detailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.in(FormalSettlementDetailFee::getFormalSettlementDetailId, detailIds)
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo)).stream()
|
||||
.collect(Collectors.groupingBy(FormalSettlementDetailFee::getFormalSettlementDetailId, LinkedHashMap::new, Collectors.toList()));
|
||||
for (FormalSettlementDetail detail : details) {
|
||||
Waybill waybill = waybillMap.get(detail.getWaybillId());
|
||||
LinkedHashSet<String> goodsNames = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> goodsTypes = new LinkedHashSet<>();
|
||||
if (waybill != null) {
|
||||
collectGoodsCargo(waybill.getGoodsJson(), goodsNames, goodsTypes);
|
||||
}
|
||||
LinkedHashSet<String> feeNames = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> feeTypes = new LinkedHashSet<>();
|
||||
for (FormalSettlementDetailFee fee : feesByDetail.getOrDefault(detail.getId(), List.of())) {
|
||||
addCargoToken(feeNames, fee.getCargoName(), true);
|
||||
addCargoToken(feeTypes, fee.getCargoType(), true);
|
||||
}
|
||||
LinkedHashSet<String> names = goodsNames.size() > 1 ? goodsNames : feeNames.size() > 1 ? feeNames : goodsNames;
|
||||
LinkedHashSet<String> types = goodsTypes.size() > 1 ? goodsTypes : feeTypes.size() > 1 ? feeTypes : goodsTypes;
|
||||
if (names.isEmpty()) {
|
||||
addCargoToken(names, detail.getCargoName(), true);
|
||||
}
|
||||
if (types.isEmpty()) {
|
||||
addCargoToken(types, detail.getCargoType(), true);
|
||||
}
|
||||
if (names.size() > 1) {
|
||||
detail.setCargoName(joinCargo(names));
|
||||
}
|
||||
if (types.size() > 1) {
|
||||
detail.setCargoType(joinCargo(types));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistDetailCargoNames(Long settlementId) {
|
||||
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, settlementId)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
Map<Long, String> cargoNames = new LinkedHashMap<>();
|
||||
Map<Long, String> cargoTypes = new LinkedHashMap<>();
|
||||
for (FormalSettlementDetail detail : details) {
|
||||
cargoNames.put(detail.getId(), detail.getCargoName());
|
||||
cargoTypes.put(detail.getId(), detail.getCargoType());
|
||||
}
|
||||
fillDetailCargoNames(details);
|
||||
for (FormalSettlementDetail detail : details) {
|
||||
if (Objects.equals(cargoNames.get(detail.getId()), detail.getCargoName())
|
||||
&& Objects.equals(cargoTypes.get(detail.getId()), detail.getCargoType())) {
|
||||
continue;
|
||||
}
|
||||
detailMapper.update(null, Wrappers.<FormalSettlementDetail>lambdaUpdate()
|
||||
.eq(FormalSettlementDetail::getId, detail.getId())
|
||||
.set(FormalSettlementDetail::getCargoName, detail.getCargoName())
|
||||
.set(FormalSettlementDetail::getCargoType, detail.getCargoType()));
|
||||
}
|
||||
}
|
||||
|
||||
private void collectGoodsCargo(String goodsJson, Set<String> names, Set<String> types) {
|
||||
if (Func.isEmpty(goodsJson)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Object parsed = JsonUtil.parse(goodsJson, List.class);
|
||||
if (!(parsed instanceof List<?> rows)) {
|
||||
return;
|
||||
}
|
||||
for (Object row : rows) {
|
||||
if (!(row instanceof Map<?, ?> goods)) {
|
||||
continue;
|
||||
}
|
||||
addCargoToken(names, goods.get("cargoName") == null ? null : String.valueOf(goods.get("cargoName")), false);
|
||||
addCargoToken(types, goods.get("cargoType") == null ? null : String.valueOf(goods.get("cargoType")), false);
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// 货物清单异常时保留明细上已有的货物名称。
|
||||
}
|
||||
}
|
||||
|
||||
private void addCargoToken(Set<String> target, String raw, boolean splitJoined) {
|
||||
if (Func.isEmpty(raw)) {
|
||||
return;
|
||||
}
|
||||
String[] parts = splitJoined ? raw.split("[,,]") : new String[]{raw};
|
||||
for (String part : parts) {
|
||||
String token = part.trim();
|
||||
if (token.isEmpty() || "手工调差".equals(token)) {
|
||||
continue;
|
||||
}
|
||||
target.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
private String joinCargo(Set<String> values) {
|
||||
String joined = String.join(",", values);
|
||||
return joined.length() > 500 ? joined.substring(0, 500) : joined;
|
||||
}
|
||||
|
||||
private void copyPreDetailFees(PreSettlementDetail source, FormalSettlementDetail target) {
|
||||
@@ -2126,14 +2340,77 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", node, reason);
|
||||
}
|
||||
|
||||
private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType,
|
||||
String beforeFeeItem, BigDecimal beforeAdjustAmount, BigDecimal beforeSettlementAmount, String beforeRemark,
|
||||
FormalSettlementSummaryFee row) {
|
||||
List<String> changes = new java.util.ArrayList<>();
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
if (creating) {
|
||||
changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】");
|
||||
beforeData.put("费用类型", "");
|
||||
beforeData.put("费用项", "");
|
||||
beforeData.put("调整金额", BigDecimal.ZERO.setScale(2));
|
||||
beforeData.put("结算金额", BigDecimal.ZERO.setScale(2));
|
||||
beforeData.put("备注", "");
|
||||
afterData.put("费用类型", row.getFeeType());
|
||||
afterData.put("费用项", row.getFeeItem());
|
||||
afterData.put("调整金额", money(row.getAdjustAmount()));
|
||||
afterData.put("结算金额", money(row.getSettlementAmount()));
|
||||
afterData.put("备注", row.getRemark());
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
return;
|
||||
}
|
||||
appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType());
|
||||
appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem());
|
||||
appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount());
|
||||
appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount());
|
||||
appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark());
|
||||
if (changes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
}
|
||||
|
||||
private void appendChange(List<String> changes, Map<String, Object> beforeData, Map<String, Object> afterData,
|
||||
String fieldName, BigDecimal before, BigDecimal after) {
|
||||
BigDecimal oldValue = money(before);
|
||||
BigDecimal newValue = money(after);
|
||||
if (oldValue.compareTo(newValue) != 0) {
|
||||
changes.add("【" + fieldName + "】从【" + oldValue + "】调整为【" + newValue + "】");
|
||||
beforeData.put(fieldName, oldValue);
|
||||
afterData.put(fieldName, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendTextChange(List<String> changes, Map<String, Object> beforeData, Map<String, Object> afterData,
|
||||
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 + "】");
|
||||
beforeData.put(fieldName, oldValue);
|
||||
afterData.put(fieldName, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
|
||||
String content, String reason) {
|
||||
saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null);
|
||||
}
|
||||
|
||||
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
|
||||
String content, String reason, Map<String, Object> beforeData, Map<String, Object> afterData) {
|
||||
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
|
||||
record.setFormalSettlementId(settlementId);
|
||||
record.setChangeType(changeType);
|
||||
record.setLineNo(lineNo);
|
||||
record.setOperationType(operationType);
|
||||
record.setChangeContent(content);
|
||||
record.setBeforeData(beforeData == null ? null : JsonUtil.toJson(beforeData));
|
||||
record.setAfterData(afterData == null ? null : JsonUtil.toJson(afterData));
|
||||
record.setChangeReason(reason);
|
||||
record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName());
|
||||
record.setChangeTime(LocalDateTime.now());
|
||||
|
||||
+284
-45
@@ -118,6 +118,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -324,6 +325,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
amount.setCumulativeAppliedAmount(cumulativeAppliedAmount.add(writeOffAmount));
|
||||
amount.setPayableAmount(settlementAmount.subtract(cumulativeAppliedAmount).subtract(writeOffAmount)
|
||||
.max(BigDecimal.ZERO));
|
||||
amount.setPaymentSequence(resolvePaymentSequence(preSettlementReference, referenceId, excludeId));
|
||||
return amount;
|
||||
}
|
||||
|
||||
@@ -383,6 +385,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
fillBillLedger(entity, request);
|
||||
validateQuota(entity);
|
||||
saveOrUpdate(entity);
|
||||
// 汇票付款在生成申请时即占用可用余额;编辑时按差额调整,避免重复扣减
|
||||
syncBillOccupation(entity);
|
||||
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
|
||||
.eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId()));
|
||||
int lineNo = 1;
|
||||
@@ -419,7 +423,16 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
@Override @Transactional(rollbackFor = Exception.class)
|
||||
public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); recordMapper.delete(Wrappers.<PaymentApplicationRecord>lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id)); settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id)); removeById(entity); }
|
||||
public void removeDraft(Long id) {
|
||||
PaymentApplication entity = editable(id);
|
||||
if (isBillPayment(entity.getPaymentMethod())) {
|
||||
releaseBillBalanceIfOccupied(entity);
|
||||
}
|
||||
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id));
|
||||
recordMapper.delete(Wrappers.<PaymentApplicationRecord>lambdaQuery().eq(PaymentApplicationRecord::getPaymentApplicationId, id));
|
||||
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery().eq(PaymentApplicationSettlement::getPaymentApplicationId, id));
|
||||
removeById(entity);
|
||||
}
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void submit(PaymentApplicationStatusRequest request) {
|
||||
@@ -434,13 +447,33 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
validateProjectAdvanceContract(entity);
|
||||
validateSelectedBill(entity, false);
|
||||
validateQuota(entity);
|
||||
// 驳回后可用余额已退回,再次提交时重新占用
|
||||
if (isBillPayment(entity.getPaymentMethod()) && activeBillUsage(entity.getId()) == null) {
|
||||
syncBillOccupation(entity);
|
||||
}
|
||||
entity.setApprovalStatus(TransportBusiConstant.REVIEWING);
|
||||
entity.setCurrentNode("财务审核");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
updateById(entity);
|
||||
refreshFormalSettlement(entity);
|
||||
}
|
||||
@Override public void returnBill(PaymentApplicationStatusRequest request) { change(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.RETURNED, "已驳回", request.getReason()); }
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void returnBill(PaymentApplicationStatusRequest request) {
|
||||
PaymentApplication entity = lockedPayment(request.getId());
|
||||
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不允许此操作");
|
||||
}
|
||||
if (isBillPayment(entity.getPaymentMethod())) {
|
||||
releaseBillBalanceIfOccupied(entity);
|
||||
}
|
||||
entity.setApprovalStatus(TransportBusiConstant.RETURNED);
|
||||
entity.setCurrentNode("已驳回");
|
||||
entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200));
|
||||
updateById(entity);
|
||||
refreshFormalSettlement(entity);
|
||||
}
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void voidBill(PaymentApplicationStatusRequest request) {
|
||||
@@ -469,7 +502,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
public void approve(PaymentApplicationStatusRequest request) {
|
||||
PaymentApplication entity = lockedPayment(request.getId());
|
||||
if (!TransportBusiConstant.REVIEWING.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批中的付款申请允许审核");
|
||||
if (isBillPayment(entity.getPaymentMethod())) useBillBalance(entity);
|
||||
// 生成申请时已扣减;仅历史未占用单据在审批通过时补扣,避免重复扣减
|
||||
if (isBillPayment(entity.getPaymentMethod()) && activeBillUsage(entity.getId()) == null) {
|
||||
useBillBalance(entity);
|
||||
}
|
||||
entity.setApprovalStatus(TransportBusiConstant.APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
|
||||
updateById(entity);
|
||||
refreshFormalSettlement(entity);
|
||||
@@ -1960,7 +1996,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
private List<PreSettlement> resolvePreSettlements(List<Long> ids) {
|
||||
if (ids.size() <= 1) {
|
||||
if (ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<PreSettlement> settlements = ids.stream().map(preSettlementMapper::selectById).toList();
|
||||
@@ -1968,9 +2004,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
|| !TransportBusiConstant.APPROVED.equals(item.getApprovalStatus()))) {
|
||||
throw new ServiceException("只能选择审批通过、未作废的预结算单");
|
||||
}
|
||||
Long contractId = settlements.get(0).getContractId();
|
||||
if (contractId == null || settlements.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()))) {
|
||||
throw new ServiceException("多选预结算单必须属于同一合同");
|
||||
if (settlements.size() > 1) {
|
||||
Long contractId = settlements.get(0).getContractId();
|
||||
if (contractId == null || settlements.stream().anyMatch(item -> !Objects.equals(contractId, item.getContractId()))) {
|
||||
throw new ServiceException("多选预结算单必须属于同一合同");
|
||||
}
|
||||
}
|
||||
return settlements;
|
||||
}
|
||||
@@ -1997,24 +2035,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
private void savePreSettlementRelations(PaymentApplication entity, List<PreSettlement> settlements) {
|
||||
if (settlements.size() > 1) {
|
||||
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId()));
|
||||
} else {
|
||||
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
|
||||
.isNotNull(PaymentApplicationSettlement::getPreSettlementId));
|
||||
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
|
||||
.isNotNull(PaymentApplicationSettlement::getPreSettlementId));
|
||||
List<PreSettlement> targetSettlements = settlements;
|
||||
if (targetSettlements.isEmpty() && entity.getPreSettlementId() != null) {
|
||||
PreSettlement single = preSettlementMapper.selectById(entity.getPreSettlementId());
|
||||
if (single != null && !Objects.equals(single.getIsDeleted(), 1)) {
|
||||
targetSettlements = List.of(single);
|
||||
}
|
||||
}
|
||||
if (targetSettlements.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BigDecimal totalBase = settlements.stream()
|
||||
// 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放)
|
||||
BigDecimal totalBase = targetSettlements.stream()
|
||||
.map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId()))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal remainingApplied = money(entity.getAppliedAmount());
|
||||
for (int index = 0; index < settlements.size(); index++) {
|
||||
PreSettlement settlement = settlements.get(index);
|
||||
for (int index = 0; index < targetSettlements.size(); index++) {
|
||||
PreSettlement settlement = targetSettlements.get(index);
|
||||
BigDecimal base = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId());
|
||||
BigDecimal applied = index == settlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0
|
||||
? remainingApplied : money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN);
|
||||
BigDecimal applied = index == targetSettlements.size() - 1 || totalBase.compareTo(BigDecimal.ZERO) == 0
|
||||
? remainingApplied
|
||||
: money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN);
|
||||
PaymentApplicationSettlement relation = new PaymentApplicationSettlement();
|
||||
relation.setPaymentApplicationId(entity.getId());
|
||||
relation.setPreSettlementId(settlement.getId());
|
||||
@@ -2064,10 +2108,24 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
private BigDecimal sumApplied(LambdaQueryWrapper<PaymentApplication> wrapper) {
|
||||
return list(wrapper.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
|
||||
return list(wrapper).stream()
|
||||
.filter(item -> !releasedOccupiedPayment(item))
|
||||
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已释放占用的付款申请:作废、审核不通过(驳回)不再占用可付款金额;
|
||||
* 删除草稿由逻辑删除自然释放。生成草稿/审批中/审批通过均占用。
|
||||
*/
|
||||
private boolean releasedOccupiedPayment(PaymentApplication payment) {
|
||||
if (payment == null || Objects.equals(payment.getIsDeleted(), 1)) {
|
||||
return true;
|
||||
}
|
||||
String status = payment.getApprovalStatus();
|
||||
return TransportBusiConstant.VOIDED.equals(status)
|
||||
|| TransportBusiConstant.RETURNED.equals(status);
|
||||
}
|
||||
|
||||
private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) {
|
||||
if ("progress_advance".equals(paymentType)) {
|
||||
return cumulativePreSettlementApplied(referenceId, excludeId);
|
||||
@@ -2078,14 +2136,88 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算单当前付款次序:历史有效付款申请次数 + 1(作废、当前单据自身不计)。
|
||||
*
|
||||
* @param preSettlementReference 是否预结算关联
|
||||
* @param referenceId 结算单主键
|
||||
* @param excludeId 排除的付款申请主键(编辑中)
|
||||
* @return 第几次付款,至少为 1
|
||||
*/
|
||||
private int resolvePaymentSequence(boolean preSettlementReference, Long referenceId, Long excludeId) {
|
||||
Set<Long> paymentIds = new LinkedHashSet<>();
|
||||
if (preSettlementReference) {
|
||||
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPreSettlementId, referenceId)
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0));
|
||||
for (PaymentApplicationSettlement relation : relations) {
|
||||
if (relation.getPaymentApplicationId() != null) {
|
||||
paymentIds.add(relation.getPaymentApplicationId());
|
||||
}
|
||||
}
|
||||
list(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getPreSettlementId, referenceId)
|
||||
.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED))
|
||||
.forEach(payment -> paymentIds.add(payment.getId()));
|
||||
} else {
|
||||
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getFormalSettlementId, referenceId)
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0));
|
||||
for (PaymentApplicationSettlement relation : relations) {
|
||||
if (relation.getPaymentApplicationId() != null) {
|
||||
paymentIds.add(relation.getPaymentApplicationId());
|
||||
}
|
||||
}
|
||||
list(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getSettlementId, referenceId)
|
||||
.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED))
|
||||
.forEach(payment -> paymentIds.add(payment.getId()));
|
||||
}
|
||||
if (paymentIds.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
long priorCount = listByIds(paymentIds.stream().toList()).stream()
|
||||
.filter(payment -> !releasedOccupiedPayment(payment))
|
||||
.filter(payment -> !Objects.equals(payment.getId(), excludeId))
|
||||
.count();
|
||||
return (int) priorCount + 1;
|
||||
}
|
||||
|
||||
private BigDecimal cumulativeFormalSettlementApplied(Long settlementId, Long excludeId) {
|
||||
return sumApplied(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
// 多选正式结算时按关联行累计占用;单选兼容主表 settlementId
|
||||
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getFormalSettlementId, settlementId)
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0));
|
||||
Set<Long> coveredPaymentIds = new HashSet<>();
|
||||
BigDecimal relationAmount = BigDecimal.ZERO;
|
||||
if (!relations.isEmpty()) {
|
||||
Map<Long, PaymentApplication> payments = listByIds(relations.stream()
|
||||
.map(PaymentApplicationSettlement::getPaymentApplicationId).filter(Objects::nonNull).distinct().toList())
|
||||
.stream().collect(Collectors.toMap(PaymentApplication::getId, item -> item, (left, right) -> left));
|
||||
for (PaymentApplicationSettlement relation : relations) {
|
||||
PaymentApplication payment = payments.get(relation.getPaymentApplicationId());
|
||||
if (releasedOccupiedPayment(payment) || Objects.equals(payment.getId(), excludeId)) {
|
||||
if (payment != null) {
|
||||
coveredPaymentIds.add(payment.getId());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
coveredPaymentIds.add(payment.getId());
|
||||
relationAmount = relationAmount.add(money(relation.getAppliedAmount()));
|
||||
}
|
||||
}
|
||||
BigDecimal legacyAmount = sumApplied(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getSettlementId, settlementId)
|
||||
.ne(excludeId != null, PaymentApplication::getId, excludeId));
|
||||
.ne(excludeId != null, PaymentApplication::getId, excludeId)
|
||||
.notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds));
|
||||
return relationAmount.add(legacyAmount);
|
||||
}
|
||||
|
||||
private BigDecimal cumulativePreSettlementApplied(Long preSettlementId, Long excludeId) {
|
||||
// 进度预付、尾款付款关联同一预结算单时累计占用可付款额度;多选时按关联行的申请金额累计,避免把整单金额算到第一张预结算上
|
||||
// 进度预付、尾款付款关联同一预结算单时累计占用可付款额度;按关联行申请金额累计,兼容历史仅写主表的单据
|
||||
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPreSettlementId, preSettlementId)
|
||||
@@ -2098,21 +2230,21 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
.stream().collect(Collectors.toMap(PaymentApplication::getId, item -> item, (left, right) -> left));
|
||||
for (PaymentApplicationSettlement relation : relations) {
|
||||
PaymentApplication payment = payments.get(relation.getPaymentApplicationId());
|
||||
if (payment == null || Objects.equals(payment.getIsDeleted(), 1)) {
|
||||
if (releasedOccupiedPayment(payment) || Objects.equals(payment.getId(), excludeId)) {
|
||||
if (payment != null) {
|
||||
coveredPaymentIds.add(payment.getId());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
coveredPaymentIds.add(payment.getId());
|
||||
if (Objects.equals(payment.getId(), excludeId) || TransportBusiConstant.VOIDED.equals(payment.getApprovalStatus())) {
|
||||
continue;
|
||||
}
|
||||
relationAmount = relationAmount.add(money(relation.getAppliedAmount()));
|
||||
}
|
||||
}
|
||||
BigDecimal legacyAmount = list(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getPreSettlementId, preSettlementId)
|
||||
.ne(excludeId != null, PaymentApplication::getId, excludeId)
|
||||
.notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)
|
||||
.ne(PaymentApplication::getApprovalStatus, TransportBusiConstant.VOIDED)).stream()
|
||||
.notIn(!coveredPaymentIds.isEmpty(), PaymentApplication::getId, coveredPaymentIds)).stream()
|
||||
.filter(item -> !releasedOccupiedPayment(item))
|
||||
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return relationAmount.add(legacyAmount);
|
||||
}
|
||||
@@ -2142,7 +2274,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
return;
|
||||
}
|
||||
BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
|
||||
entity.setBillLedgerId(ledger.getId());
|
||||
entity.setBillNo(ledger.getBillNo());
|
||||
}
|
||||
@@ -2150,37 +2282,130 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private void validateSelectedBill(PaymentApplication entity, boolean locked) {
|
||||
if (!isBillPayment(entity.getPaymentMethod())) return;
|
||||
BillLedger ledger = locked ? lockedBill(entity.getBillLedgerId()) : billLedgerMapper.selectById(entity.getBillLedgerId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, entity.getId());
|
||||
}
|
||||
|
||||
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity) {
|
||||
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity, Long paymentApplicationId) {
|
||||
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
|
||||
if (!"received".equals(ledger.getBillType())) {
|
||||
throw new ServiceException("汇票付款仅可选择收票类型的汇票");
|
||||
}
|
||||
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
|
||||
throw new ServiceException("所选汇票已到期");
|
||||
}
|
||||
if (money(amount).compareTo(money(ledger.getAvailableBalance())) > 0) throw new ServiceException("申请付款金额超过汇票可用余额");
|
||||
BigDecimal available = money(ledger.getAvailableBalance()).add(occupiedAmount(paymentApplicationId, ledger.getId()));
|
||||
if (money(amount).compareTo(available) > 0) throw new ServiceException("申请付款金额超过汇票可用余额");
|
||||
if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
|
||||
}
|
||||
|
||||
private void useBillBalance(PaymentApplication entity) {
|
||||
BillLedger ledger = lockedBill(entity.getBillLedgerId());
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
|
||||
BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
||||
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE"));
|
||||
if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录");
|
||||
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true, null);
|
||||
BillLedgerUsage exists = lockedBillUsage(entity.getId());
|
||||
if (exists != null && TransportBusiConstant.APPROVED.equals(exists.getUsageStatus())) {
|
||||
throw new ServiceException("该付款申请已生成汇票使用记录");
|
||||
}
|
||||
BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
|
||||
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount));
|
||||
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount).setScale(2, RoundingMode.HALF_UP));
|
||||
billLedgerMapper.updateById(ledger);
|
||||
if (exists == null) {
|
||||
BillLedgerUsage usage = new BillLedgerUsage();
|
||||
usage.setBillLedgerId(ledger.getId());
|
||||
usage.setPaymentApplicationId(entity.getId());
|
||||
usage.setApplicationNo(entity.getPaymentNo());
|
||||
usage.setUsedAmount(amount);
|
||||
usage.setUseDeptId(entity.getDeptId());
|
||||
usage.setUseDeptName(entity.getDeptName());
|
||||
usage.setUsageStatus(TransportBusiConstant.APPROVED);
|
||||
usage.setStatus(1);
|
||||
billLedgerUsageMapper.insert(usage);
|
||||
return;
|
||||
}
|
||||
exists.setBillLedgerId(ledger.getId());
|
||||
exists.setApplicationNo(entity.getPaymentNo());
|
||||
exists.setUsedAmount(amount);
|
||||
exists.setUseDeptId(entity.getDeptId());
|
||||
exists.setUseDeptName(entity.getDeptName());
|
||||
exists.setUsageStatus(TransportBusiConstant.APPROVED);
|
||||
billLedgerUsageMapper.updateById(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇票付款申请生成后占用汇票可用余额。同一申请重复保存时只调整差额;
|
||||
* 更换汇票或改为非汇票付款时,先退回原占用。
|
||||
*/
|
||||
private void syncBillOccupation(PaymentApplication entity) {
|
||||
BillLedgerUsage usage = lockedBillUsage(entity.getId());
|
||||
boolean occupy = isBillPayment(entity.getPaymentMethod()) && entity.getBillLedgerId() != null;
|
||||
if (!occupy) {
|
||||
if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) {
|
||||
releaseUsage(usage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
|
||||
if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())
|
||||
&& Objects.equals(usage.getBillLedgerId(), entity.getBillLedgerId())) {
|
||||
adjustOccupiedAmount(usage, entity, amount);
|
||||
return;
|
||||
}
|
||||
if (usage != null && TransportBusiConstant.APPROVED.equals(usage.getUsageStatus())) {
|
||||
releaseUsage(usage);
|
||||
}
|
||||
useBillBalance(entity);
|
||||
}
|
||||
|
||||
private void adjustOccupiedAmount(BillLedgerUsage usage, PaymentApplication entity, BigDecimal amount) {
|
||||
BigDecimal delta = amount.subtract(money(usage.getUsedAmount())).setScale(2, RoundingMode.HALF_UP);
|
||||
BillLedger ledger = lockedBill(usage.getBillLedgerId());
|
||||
if (delta.compareTo(BigDecimal.ZERO) > 0 && delta.compareTo(money(ledger.getAvailableBalance())) > 0) {
|
||||
throw new ServiceException("申请付款金额超过汇票可用余额");
|
||||
}
|
||||
BigDecimal available = money(ledger.getAvailableBalance()).subtract(delta).setScale(2, RoundingMode.HALF_UP);
|
||||
if (available.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new ServiceException("申请付款金额超过汇票可用余额");
|
||||
}
|
||||
BigDecimal faceAmount = money(ledger.getFaceAmount());
|
||||
if (available.compareTo(faceAmount) > 0) {
|
||||
available = faceAmount;
|
||||
}
|
||||
ledger.setAvailableBalance(available);
|
||||
billLedgerMapper.updateById(ledger);
|
||||
BillLedgerUsage usage = new BillLedgerUsage();
|
||||
usage.setBillLedgerId(ledger.getId());
|
||||
usage.setPaymentApplicationId(entity.getId());
|
||||
usage.setApplicationNo(entity.getPaymentNo());
|
||||
usage.setUsedAmount(amount);
|
||||
usage.setUseDeptId(entity.getDeptId());
|
||||
usage.setUseDeptName(entity.getDeptName());
|
||||
usage.setUsageStatus(TransportBusiConstant.APPROVED);
|
||||
usage.setStatus(1);
|
||||
billLedgerUsageMapper.insert(usage);
|
||||
billLedgerUsageMapper.updateById(usage);
|
||||
}
|
||||
|
||||
private BigDecimal occupiedAmount(Long paymentApplicationId, Long billLedgerId) {
|
||||
if (paymentApplicationId == null || billLedgerId == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BillLedgerUsage usage = activeBillUsage(paymentApplicationId);
|
||||
if (usage == null || !Objects.equals(usage.getBillLedgerId(), billLedgerId)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return money(usage.getUsedAmount());
|
||||
}
|
||||
|
||||
private BillLedgerUsage activeBillUsage(Long paymentApplicationId) {
|
||||
if (paymentApplicationId == null) {
|
||||
return null;
|
||||
}
|
||||
return billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
||||
.eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId)
|
||||
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED)
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
private BillLedgerUsage lockedBillUsage(Long paymentApplicationId) {
|
||||
if (paymentApplicationId == null) {
|
||||
return null;
|
||||
}
|
||||
return billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
||||
.eq(BillLedgerUsage::getPaymentApplicationId, paymentApplicationId)
|
||||
.last("limit 1 FOR UPDATE"));
|
||||
}
|
||||
|
||||
private void releaseBillBalance(PaymentApplication entity) {
|
||||
@@ -2188,9 +2413,23 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
|
||||
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE"));
|
||||
if (usage == null) throw new ServiceException("未找到对应的汇票使用记录,无法作废");
|
||||
releaseUsage(usage);
|
||||
}
|
||||
|
||||
private void releaseBillBalanceIfOccupied(PaymentApplication entity) {
|
||||
BillLedgerUsage usage = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
|
||||
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
|
||||
.eq(BillLedgerUsage::getUsageStatus, TransportBusiConstant.APPROVED).last("FOR UPDATE"));
|
||||
if (usage == null) {
|
||||
return;
|
||||
}
|
||||
releaseUsage(usage);
|
||||
}
|
||||
|
||||
private void releaseUsage(BillLedgerUsage usage) {
|
||||
BillLedger ledger = lockedBill(usage.getBillLedgerId());
|
||||
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount()))
|
||||
.min(money(ledger.getFaceAmount())));
|
||||
.min(money(ledger.getFaceAmount())).setScale(2, RoundingMode.HALF_UP));
|
||||
billLedgerMapper.updateById(ledger);
|
||||
usage.setUsageStatus("released");
|
||||
billLedgerUsageMapper.updateById(usage);
|
||||
|
||||
+183
-22
@@ -307,7 +307,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
IPage<ReceivablePayableDetail> sourcePage = sourceDetailMapper.selectPage(
|
||||
new Page<>(page.getCurrent(), page.getSize()), wrapper);
|
||||
Page<Map<String, Object>> result = new Page<>(sourcePage.getCurrent(), sourcePage.getSize(), sourcePage.getTotal());
|
||||
result.setRecords(sourcePage.getRecords().stream().map(this::candidateMap).toList());
|
||||
List<Map<String, Object>> records = sourcePage.getRecords().stream().map(this::candidateMap).toList();
|
||||
fillCandidateBillingTypes(records);
|
||||
result.setRecords(records);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -619,7 +621,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
if (Func.isEmpty(request.getRows())) {
|
||||
throw new ServiceException("请填写需要调整的费用行");
|
||||
}
|
||||
String changeReason = limitRemark(request.getChangeReason(), 200);
|
||||
// 弹窗底部备注非必填,写入结算明细备注
|
||||
String detailRemark = limitRemark(request.getChangeReason(), 200);
|
||||
String beforeDetailRemark = detail.getRemark();
|
||||
Map<Long, PreSettlementDetailFee> existingMap = detailFees(detail.getId()).stream()
|
||||
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity()));
|
||||
if (request.getRows().size() != existingMap.size()) {
|
||||
@@ -639,7 +643,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
BigDecimal beforeFreight = fee.getFreightAmount();
|
||||
BigDecimal beforeAmount = money(fee.getSettlementAmountTax());
|
||||
BigDecimal beforeNoTaxAmount = fee.getSettlementAmountNoTax();
|
||||
String beforeRemark = fee.getRemark();
|
||||
Map<String, BigDecimal> beforeFeeItems = parseFeeItems(fee.getFeeItemsJson());
|
||||
fee.setTransportQuantity(nonNegative(requestRow.getTransportQuantity(), "运输总量"));
|
||||
fee.setMileage(nonNegative(requestRow.getMileage(), "里程"));
|
||||
@@ -652,7 +655,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
fee.setSettlementAmountNoTax(requestRow.getSettlementAmountNoTax() == null ? null
|
||||
: money(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, beforeData, afterData, prefix + "运输总量", beforeQuantity, fee.getTransportQuantity());
|
||||
@@ -667,16 +669,19 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
appendChange(changes, beforeData, afterData, prefix + "结算金额(含税)", beforeAmount, afterAmount);
|
||||
appendChange(changes, beforeData, afterData, prefix + "结算金额(不含税)", beforeNoTaxAmount,
|
||||
fee.getSettlementAmountNoTax());
|
||||
appendTextChange(changes, beforeData, afterData, prefix + "备注", beforeRemark, fee.getRemark());
|
||||
}
|
||||
appendTextChange(changes, beforeData, afterData, "备注", beforeDetailRemark, detailRemark);
|
||||
if (changes.isEmpty()) {
|
||||
throw new ServiceException("未修改任何结算明细费用");
|
||||
}
|
||||
refreshDetail(detail);
|
||||
// refreshDetail 会按费用行回填备注,需在其后覆盖为弹窗底部备注
|
||||
detail.setRemark(detailRemark);
|
||||
detailMapper.updateById(detail);
|
||||
rebuildSummaryFees(settlement.getId(), true);
|
||||
refreshSettlementAmount(settlement);
|
||||
saveChange(settlement.getId(), "结算明细项", detail.getLineNo(), "调整",
|
||||
String.join(";", changes), changeReason, beforeData, afterData);
|
||||
String.join(";", changes), detailRemark, beforeData, afterData);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -755,6 +760,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
result.put("quantityUnit", source.getQuantityUnit());
|
||||
result.put("mileage", source.getMileage());
|
||||
result.put("batchNo", source.getBatchNo());
|
||||
result.put("billingType", "");
|
||||
result.put("unitPrice", source.getUnitPrice());
|
||||
result.put("freightAmount", money(source.getFreightAmount()));
|
||||
result.put("feeItemsJson", source.getFeeItemsJson());
|
||||
@@ -768,6 +774,35 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 候选明细补充计费方式(货物费用计费类型去重汇总) */
|
||||
private void fillCandidateBillingTypes(List<Map<String, Object>> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> detailIds = records.stream()
|
||||
.map(item -> Func.toLong(item.get("id")))
|
||||
.filter(id -> id != null && id > 0)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (detailIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> billingTypeMap = sourceFeeMapper.selectList(
|
||||
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.in(ReceivablePayableCargoFee::getDetailId, detailIds)
|
||||
.orderByAsc(ReceivablePayableCargoFee::getLineNo))
|
||||
.stream()
|
||||
.filter(fee -> Func.isNotEmpty(fee.getBillingType()))
|
||||
.collect(Collectors.groupingBy(ReceivablePayableCargoFee::getDetailId,
|
||||
Collectors.mapping(ReceivablePayableCargoFee::getBillingType,
|
||||
Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new),
|
||||
values -> String.join("、", values)))));
|
||||
for (Map<String, Object> record : records) {
|
||||
Long detailId = Func.toLong(record.get("id"));
|
||||
record.put("billingType", billingTypeMap.getOrDefault(detailId, ""));
|
||||
}
|
||||
}
|
||||
|
||||
private void synchronizeDetails(PreSettlement settlement, List<Long> requestedIds, boolean allowSourceMismatch) {
|
||||
List<Long> distinctIds = requestedIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
List<PreSettlementDetail> existingDetails = listDetails(settlement.getId());
|
||||
@@ -1004,13 +1039,30 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryFeeMapper.delete(Wrappers.<PreSettlementSummaryFee>lambdaQuery()
|
||||
.eq(PreSettlementSummaryFee::getPreSettlementId, settlementId)
|
||||
.eq(PreSettlementSummaryFee::getManualFlag, 0));
|
||||
int lineNo = 1;
|
||||
for (Map.Entry<String, BigDecimal> entry : aggregates.entrySet()) {
|
||||
// 尽量沿用已有系统归集行的序号,避免重建后与手工费用交错顺序被打乱。
|
||||
List<Map.Entry<String, BigDecimal>> orderedAggregates = new ArrayList<>(aggregates.entrySet());
|
||||
orderedAggregates.sort((left, right) -> {
|
||||
PreSettlementSummaryFee leftOld = existingGenerated.get(left.getKey());
|
||||
PreSettlementSummaryFee rightOld = existingGenerated.get(right.getKey());
|
||||
Integer leftLineNo = leftOld == null ? null : leftOld.getLineNo();
|
||||
Integer rightLineNo = rightOld == null ? null : rightOld.getLineNo();
|
||||
if (leftLineNo == null && rightLineNo == null) {
|
||||
return 0;
|
||||
}
|
||||
if (leftLineNo == null) {
|
||||
return 1;
|
||||
}
|
||||
if (rightLineNo == null) {
|
||||
return -1;
|
||||
}
|
||||
return Integer.compare(leftLineNo, rightLineNo);
|
||||
});
|
||||
for (Map.Entry<String, BigDecimal> entry : orderedAggregates) {
|
||||
String[] keyParts = entry.getKey().split("\\|", 2);
|
||||
PreSettlementSummaryFee old = existingGenerated.get(entry.getKey());
|
||||
PreSettlementSummaryFee row = new PreSettlementSummaryFee();
|
||||
row.setPreSettlementId(settlementId);
|
||||
row.setLineNo(lineNo++);
|
||||
row.setLineNo(old == null || old.getLineNo() == null ? Integer.MAX_VALUE : old.getLineNo());
|
||||
row.setFeeType(keyParts[0]);
|
||||
row.setFeeItem(keyParts.length > 1 ? keyParts[1] : "");
|
||||
// 原金额是明细首次生成时的快照,多次调整后不得被当前结算金额覆盖。
|
||||
@@ -1026,7 +1078,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
}
|
||||
|
||||
private void applySummaryRequest(Long settlementId, List<PreSettlementSaveRequest.SummaryFee> requestRows) {
|
||||
if (requestRows == null) return;
|
||||
if (requestRows == null) {
|
||||
return;
|
||||
}
|
||||
// 空列表表示沿用 rebuildSummaryFees 结果(如更换合同后),不得再按空请求删光合计行。
|
||||
if (requestRows.isEmpty()) {
|
||||
renumberSummaryFees(settlementId);
|
||||
return;
|
||||
}
|
||||
Map<String, Set<String>> allowedManualFees = new LinkedHashMap<>();
|
||||
if (requestRows.stream().anyMatch(row -> Integer.valueOf(1).equals(row.getManualFlag()))) {
|
||||
for (Map<String, Object> option : feeOptions()) {
|
||||
@@ -1046,6 +1105,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
List<PreSettlementSummaryFee> availableGeneratedRows = new ArrayList<>(existingGeneratedRows);
|
||||
Set<Long> retainedManualIds = new LinkedHashSet<>();
|
||||
Set<Long> retainedGeneratedIds = new LinkedHashSet<>();
|
||||
int lineNo = 1;
|
||||
for (PreSettlementSaveRequest.SummaryFee requestRow : requestRows) {
|
||||
if (Integer.valueOf(1).equals(requestRow.getManualFlag())) {
|
||||
String feeType = requiredText(requestRow.getFeeType(), "费用类型");
|
||||
@@ -1062,11 +1122,20 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
&& Objects.equals(item.getFeeItem(), requestRow.getFeeItem()))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
if (row == null && requestRow.getId() == null) row = new PreSettlementSummaryFee();
|
||||
if (row == null && requestRow.getId() == null) {
|
||||
row = new PreSettlementSummaryFee();
|
||||
}
|
||||
if (row == null || (row.getId() != null && !Integer.valueOf(1).equals(row.getManualFlag()))) {
|
||||
throw new ServiceException("存在无效的手工费用行");
|
||||
}
|
||||
boolean creating = row.getId() == null;
|
||||
String beforeFeeType = creating ? null : row.getFeeType();
|
||||
String beforeFeeItem = creating ? null : row.getFeeItem();
|
||||
BigDecimal beforeAdjustAmount = creating ? null : money(row.getAdjustAmount());
|
||||
BigDecimal beforeSettlementAmount = creating ? null : money(row.getSettlementAmount());
|
||||
String beforeRemark = creating ? null : row.getRemark();
|
||||
row.setPreSettlementId(settlementId);
|
||||
row.setLineNo(lineNo++);
|
||||
row.setFeeType(feeType);
|
||||
row.setFeeItem(feeItem);
|
||||
row.setOriginalAmount(BigDecimal.ZERO.setScale(2));
|
||||
@@ -1074,10 +1143,14 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
row.setSettlementAmount(row.getAdjustAmount());
|
||||
row.setRemark(limitRemark(requestRow.getRemark(), 50));
|
||||
row.setManualFlag(1);
|
||||
if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row);
|
||||
if (creating) {
|
||||
summaryFeeMapper.insert(row);
|
||||
} else {
|
||||
summaryFeeMapper.updateById(row);
|
||||
}
|
||||
retainedManualIds.add(row.getId());
|
||||
saveChange(settlementId, "合计费用项", null, requestRow.getId() == null ? "新增" : "调整",
|
||||
row.getFeeItem() + "金额" + row.getSettlementAmount(), "");
|
||||
saveManualSummaryFeeChange(settlementId, creating, beforeFeeType, beforeFeeItem,
|
||||
beforeAdjustAmount, beforeSettlementAmount, beforeRemark, row);
|
||||
continue;
|
||||
}
|
||||
String feeItem = requiredText(requestRow.getFeeItem(), "费用项");
|
||||
@@ -1097,6 +1170,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
row.setPreSettlementId(settlementId);
|
||||
row.setManualFlag(0);
|
||||
}
|
||||
row.setLineNo(lineNo++);
|
||||
row.setFeeType(feeType);
|
||||
row.setFeeItem(feeItem);
|
||||
// 生成费用行的三个金额必须来源于结算明细快照,不能接受前端回传值覆盖原金额。
|
||||
@@ -1109,15 +1183,37 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
if (row.getId() == null) summaryFeeMapper.insert(row); else summaryFeeMapper.updateById(row);
|
||||
retainedGeneratedIds.add(row.getId());
|
||||
if (before.compareTo(row.getAdjustAmount()) != 0) {
|
||||
saveChange(settlementId, "合计费用项", null, "调整",
|
||||
"【调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】", "");
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
beforeData.put("费用项", row.getFeeItem());
|
||||
beforeData.put("调整金额", before);
|
||||
beforeData.put("结算金额", money(row.getOriginalAmount().add(before)));
|
||||
afterData.put("费用项", row.getFeeItem());
|
||||
afterData.put("调整金额", row.getAdjustAmount());
|
||||
afterData.put("结算金额", money(row.getSettlementAmount()));
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整",
|
||||
"【" + row.getFeeItem() + "-调整金额】从【" + before + "】调整为【" + row.getAdjustAmount() + "】",
|
||||
"", beforeData, afterData);
|
||||
}
|
||||
}
|
||||
for (PreSettlementSummaryFee manualRow : manualRows) {
|
||||
if (!retainedManualIds.contains(manualRow.getId())) {
|
||||
summaryFeeMapper.deleteById(manualRow.getId());
|
||||
saveChange(settlementId, "合计费用项", null, "删除",
|
||||
"删除" + manualRow.getFeeItem() + "费用" + manualRow.getSettlementAmount(), "");
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
beforeData.put("费用类型", manualRow.getFeeType());
|
||||
beforeData.put("费用项", manualRow.getFeeItem());
|
||||
beforeData.put("调整金额", money(manualRow.getAdjustAmount()));
|
||||
beforeData.put("结算金额", money(manualRow.getSettlementAmount()));
|
||||
beforeData.put("备注", manualRow.getRemark());
|
||||
afterData.put("费用类型", "");
|
||||
afterData.put("费用项", "");
|
||||
afterData.put("调整金额", BigDecimal.ZERO.setScale(2));
|
||||
afterData.put("结算金额", BigDecimal.ZERO.setScale(2));
|
||||
afterData.put("备注", "");
|
||||
saveChange(settlementId, "合计费用项", manualRow.getLineNo(), "删除",
|
||||
"删除合计费用项【" + manualRow.getFeeItem() + "】金额【" + money(manualRow.getSettlementAmount()) + "】",
|
||||
"", beforeData, afterData);
|
||||
}
|
||||
}
|
||||
for (PreSettlementSummaryFee generatedRow : existingGeneratedRows) {
|
||||
@@ -1125,32 +1221,63 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
summaryFeeMapper.deleteById(generatedRow.getId());
|
||||
}
|
||||
}
|
||||
renumberSummaryFees(settlementId);
|
||||
// lineNo 已按前端提交顺序写入,不再按旧序号重排,避免手工费用与系统归集费用顺序被打乱。
|
||||
}
|
||||
|
||||
private void refreshDetail(PreSettlementDetail detail) {
|
||||
List<PreSettlementDetailFee> fees = detailFees(detail.getId());
|
||||
BigDecimal transportQuantity = fees.stream().map(PreSettlementDetailFee::getTransportQuantity)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal mileage = fees.stream().map(PreSettlementDetailFee::getMileage)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
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);
|
||||
BigDecimal settlementAmountNoTax = fees.stream().map(PreSettlementDetailFee::getSettlementAmountNoTax)
|
||||
.filter(Objects::nonNull).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
fees.forEach(fee -> parseFeeItems(fee.getFeeItemsJson()).forEach((name, amount) ->
|
||||
feeItems.merge(name, money(amount), BigDecimal::add)));
|
||||
// 运输单价无法按行简单求和,与前端暂存逻辑一致:取首行费用单价。
|
||||
BigDecimal unitPrice = fees.stream().map(PreSettlementDetailFee::getUnitPrice)
|
||||
.filter(Objects::nonNull).findFirst().orElse(null);
|
||||
String remark = fees.stream().map(PreSettlementDetailFee::getRemark)
|
||||
.filter(Func::isNotEmpty).findFirst().orElse(detail.getRemark());
|
||||
detail.setTransportQuantity(money(transportQuantity));
|
||||
detail.setMileage(mileage);
|
||||
detail.setUnitPrice(unitPrice);
|
||||
detail.setFreightAmount(money(freightAmount));
|
||||
detail.setOriginalAmount(money(originalAmount));
|
||||
detail.setAdjustAmount(money(settlementAmountTax.subtract(originalAmount)));
|
||||
detail.setSettlementAmountTax(money(settlementAmountTax));
|
||||
detail.setSettlementAmountNoTax(fees.stream().anyMatch(fee -> fee.getSettlementAmountNoTax() != null)
|
||||
? money(settlementAmountNoTax) : null);
|
||||
detail.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
detail.setRemark(limitRemark(remark, 200));
|
||||
detailMapper.updateById(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算金额以结算合计为准(含手工新增费用),与详情页合计及付款申请取数口径一致。
|
||||
* 合计行为空时回退为明细含税金额合计。
|
||||
*/
|
||||
private void refreshSettlementAmount(PreSettlement settlement) {
|
||||
BigDecimal settlementAmount = listDetails(settlement.getId()).stream()
|
||||
.map(PreSettlementDetail::getSettlementAmountTax).map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
List<PreSettlementSummaryFee> summaryFees = listSummaryFees(settlement.getId());
|
||||
BigDecimal settlementAmount;
|
||||
if (!summaryFees.isEmpty()) {
|
||||
settlementAmount = summaryFees.stream()
|
||||
.map(PreSettlementSummaryFee::getSettlementAmount)
|
||||
.map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
} else {
|
||||
settlementAmount = listDetails(settlement.getId()).stream()
|
||||
.map(PreSettlementDetail::getSettlementAmountTax)
|
||||
.map(this::money)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
settlement.setSettlementAmount(money(settlementAmount));
|
||||
BigDecimal rate = normalizeRate(settlement.getExchangeRate(), settlement.getCurrency());
|
||||
settlement.setExchangeRate(rate);
|
||||
@@ -1408,6 +1535,40 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
}
|
||||
}
|
||||
|
||||
private void saveManualSummaryFeeChange(Long settlementId, boolean creating, String beforeFeeType,
|
||||
String beforeFeeItem, BigDecimal beforeAdjustAmount, BigDecimal beforeSettlementAmount, String beforeRemark,
|
||||
PreSettlementSummaryFee row) {
|
||||
List<String> changes = new ArrayList<>();
|
||||
Map<String, Object> beforeData = new LinkedHashMap<>();
|
||||
Map<String, Object> afterData = new LinkedHashMap<>();
|
||||
if (creating) {
|
||||
changes.add("新增合计费用项【" + row.getFeeItem() + "】金额【" + money(row.getSettlementAmount()) + "】");
|
||||
beforeData.put("费用类型", "");
|
||||
beforeData.put("费用项", "");
|
||||
beforeData.put("调整金额", BigDecimal.ZERO.setScale(2));
|
||||
beforeData.put("结算金额", BigDecimal.ZERO.setScale(2));
|
||||
beforeData.put("备注", "");
|
||||
afterData.put("费用类型", row.getFeeType());
|
||||
afterData.put("费用项", row.getFeeItem());
|
||||
afterData.put("调整金额", money(row.getAdjustAmount()));
|
||||
afterData.put("结算金额", money(row.getSettlementAmount()));
|
||||
afterData.put("备注", row.getRemark());
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "新增", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
return;
|
||||
}
|
||||
appendTextChange(changes, beforeData, afterData, "费用类型", beforeFeeType, row.getFeeType());
|
||||
appendTextChange(changes, beforeData, afterData, "费用项", beforeFeeItem, row.getFeeItem());
|
||||
appendChange(changes, beforeData, afterData, "调整金额", beforeAdjustAmount, row.getAdjustAmount());
|
||||
appendChange(changes, beforeData, afterData, "结算金额", beforeSettlementAmount, row.getSettlementAmount());
|
||||
appendTextChange(changes, beforeData, afterData, "备注", beforeRemark, row.getRemark());
|
||||
if (changes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
saveChange(settlementId, "合计费用项", row.getLineNo(), "调整", String.join(";", changes), "",
|
||||
beforeData, afterData);
|
||||
}
|
||||
|
||||
private void saveChange(Long settlementId, String changeType, Integer lineNo, String operationType,
|
||||
String content, String reason) {
|
||||
saveChange(settlementId, changeType, lineNo, operationType, content, reason, null, null);
|
||||
|
||||
+15
-5
@@ -82,6 +82,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
private static final String STATUS_APPROVED = "approved";
|
||||
private static final String STATUS_REJECTED = "rejected";
|
||||
private static final String STATUS_CHANGE_REVIEWING = "change_reviewing";
|
||||
private static final String STATUS_CHANGE_APPROVED = "change_approved";
|
||||
private static final String STATUS_VOIDED = "voided";
|
||||
private static final String EFFECTIVE_TEMPORARY = "temporary";
|
||||
private static final String EFFECTIVE_FORMAL = "formal";
|
||||
@@ -206,7 +207,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
if (!List.of(STATUS_REVIEWING, STATUS_CHANGE_REVIEWING).contains(projectApply.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不允许审批通过");
|
||||
}
|
||||
projectApply.setApprovalStatus(STATUS_CHANGE_REVIEWING.equals(projectApply.getApprovalStatus()) ? "change_approved" : STATUS_APPROVED);
|
||||
projectApply.setApprovalStatus(STATUS_CHANGE_REVIEWING.equals(projectApply.getApprovalStatus()) ? STATUS_CHANGE_APPROVED : STATUS_APPROVED);
|
||||
projectApply.setEffectiveType(EFFECTIVE_FORMAL);
|
||||
projectApply.setCurrentNode("审批通过");
|
||||
projectApply.setCurrentProcessor(AuthUtil.getUserName());
|
||||
@@ -244,7 +245,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean voidProject(Long id, String reason) {
|
||||
ProjectApply projectApply = loadExists(id);
|
||||
if (!Objects.equals(projectApply.getApprovalStatus(), STATUS_APPROVED)) {
|
||||
if (!List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED).contains(projectApply.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过项目允许作废");
|
||||
}
|
||||
projectApply.setApprovalStatus(STATUS_VOIDED);
|
||||
@@ -260,6 +261,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
// 变更草稿仅更新业务字段,不生成变更记录;变更记录在 submitChange 时落库
|
||||
ProjectApply oldRecord = loadChangeable(projectApply.getId());
|
||||
prepare(projectApply);
|
||||
normalizeUndertakeDept(projectApply);
|
||||
copyChangeFields(oldRecord, projectApply);
|
||||
validateSubmit(oldRecord);
|
||||
validateChangeLength(oldRecord);
|
||||
@@ -272,6 +274,7 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
ProjectApply oldRecord = loadChangeable(projectApply.getId());
|
||||
Map<String, Object> beforeData = buildProjectChangeSnapshot(oldRecord);
|
||||
prepare(projectApply);
|
||||
normalizeUndertakeDept(projectApply);
|
||||
copyChangeFields(oldRecord, projectApply);
|
||||
validateSubmit(oldRecord);
|
||||
validateChange(oldRecord, projectApply.getChangeType());
|
||||
@@ -379,13 +382,13 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
if (Boolean.TRUE.equals(projectApply.getTemporaryCreditLimitSelectable())) {
|
||||
queryWrapper.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL)
|
||||
.in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved");
|
||||
.in(ProjectApply::getApprovalStatus, STATUS_APPROVED, STATUS_CHANGE_APPROVED);
|
||||
}
|
||||
if (Boolean.TRUE.equals(projectApply.getContractSelectable())) {
|
||||
// 正式审批通过项目 + 未作废的临时项目均可被合同选择
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.nested(formal -> formal.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL)
|
||||
.in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved"))
|
||||
.in(ProjectApply::getApprovalStatus, STATUS_APPROVED, STATUS_CHANGE_APPROVED))
|
||||
.or(temporary -> temporary.eq(ProjectApply::getEffectiveType, EFFECTIVE_TEMPORARY)
|
||||
.ne(ProjectApply::getApprovalStatus, STATUS_VOIDED)));
|
||||
}
|
||||
@@ -559,6 +562,11 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
|
||||
private void copyChangeFields(ProjectApply target, ProjectApply source) {
|
||||
// 项目变更允许调整业务部门、平台公司
|
||||
target.setBusinessDeptId(source.getBusinessDeptId());
|
||||
target.setBusinessDeptName(source.getBusinessDeptName());
|
||||
target.setUndertakeDeptId(source.getUndertakeDeptId());
|
||||
target.setUndertakeDeptName(source.getUndertakeDeptName());
|
||||
target.setFundLimit(source.getFundLimit());
|
||||
target.setReceivableLimit(source.getReceivableLimit());
|
||||
target.setReceivableDays(source.getReceivableDays());
|
||||
@@ -875,7 +883,9 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
}
|
||||
|
||||
private void validateChangeable(ProjectApply projectApply) {
|
||||
if (!Objects.equals(projectApply.getApprovalStatus(), STATUS_APPROVED) || !Objects.equals(projectApply.getEffectiveType(), EFFECTIVE_FORMAL)) {
|
||||
// 首次立项通过(approved)与历次变更通过(change_approved)均可再次发起变更
|
||||
boolean approved = List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED).contains(projectApply.getApprovalStatus());
|
||||
if (!approved || !Objects.equals(projectApply.getEffectiveType(), EFFECTIVE_FORMAL)) {
|
||||
throw new ServiceException("仅正式生效且审批通过的项目允许发起变更");
|
||||
}
|
||||
}
|
||||
|
||||
+113
-16
@@ -836,6 +836,12 @@ public class ReceivablePayableDetailServiceImpl
|
||||
if (existsByWaybill(waybill.getId(), targetSettlementType)) {
|
||||
continue;
|
||||
}
|
||||
if ("payable".equals(targetSettlementType)
|
||||
&& trimToNull(waybill.getCarrierName()) != null
|
||||
&& !matchesWaybillCarrier(contract, waybill)) {
|
||||
throw new ServiceException("运单【" + Func.toStr(waybill.getWaybillNo(), String.valueOf(waybill.getId()))
|
||||
+ "】的承运商不是所选承运商合同的乙方,不能使用项目内其他承运商合同生成应付");
|
||||
}
|
||||
ReceivablePayableDetail detail = buildDetail(waybill, contract, request.getBillingPlanId(), targetSettlementType);
|
||||
detail.setSourceType(SOURCE_MANUAL_GENERATION);
|
||||
save(detail);
|
||||
@@ -1064,11 +1070,20 @@ public class ReceivablePayableDetailServiceImpl
|
||||
List<Waybill> payableWaybills = waybills.stream()
|
||||
.filter(waybill -> !isSelfTransport(waybill)).toList();
|
||||
if (payableWaybills.isEmpty()) return;
|
||||
List<Waybill> carrierMatchedWaybills = payableWaybills.stream()
|
||||
.filter(waybill -> matchesWaybillCarrier(carrierContract, waybill)).toList();
|
||||
List<Waybill> otherCarrierWaybills = payableWaybills.stream()
|
||||
.filter(waybill -> !matchesWaybillCarrier(carrierContract, waybill)).toList();
|
||||
if (!otherCarrierWaybills.isEmpty()) {
|
||||
generateAutomaticWaybillDetails(otherCarrierWaybills, false,
|
||||
this::resolveCarrierContract, currentUserId, generateTime);
|
||||
}
|
||||
if (carrierMatchedWaybills.isEmpty()) return;
|
||||
generateAutomaticDetail(carrierContract, "payable",
|
||||
() -> existsByLoading(loadingNo, "payable"), payableWaybills,
|
||||
() -> existsByLoading(loadingNo, "payable"), carrierMatchedWaybills,
|
||||
(fees, unitPrice) -> {
|
||||
normalizeWaybillFeeLines(fees);
|
||||
return buildLoadingDetail(loadingNo, carrierContract, payableWaybills, fees, unitPrice);
|
||||
return buildLoadingDetail(loadingNo, carrierContract, carrierMatchedWaybills, fees, unitPrice);
|
||||
},
|
||||
false, currentUserId, generateTime);
|
||||
}
|
||||
@@ -1118,22 +1133,54 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
private ContractManage resolveWaybillCarrierContract(Waybill waybill) {
|
||||
if (Func.isNotEmpty(waybill.getLoadingNo())) return null;
|
||||
return resolveCarrierContract(waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应付只认运单选择的承运商。
|
||||
* 运单已记录承运商合同时,乙方必须就是该承运商;否则按承运商名称回查同一项目下的承运商合同。
|
||||
* 不允许落到项目里其他承运商合同的客商。
|
||||
*/
|
||||
private ContractManage resolveCarrierContract(Waybill waybill) {
|
||||
if (waybill == null || isSelfTransport(waybill)) {
|
||||
return null;
|
||||
}
|
||||
String carrierName = trimToNull(waybill.getCarrierName());
|
||||
if (Func.isNotEmpty(waybill.getCarrierContractId())) {
|
||||
ContractManage contract = contractManageService.getById(waybill.getCarrierContractId());
|
||||
if (contract == null || !"承运商合同".equals(contract.getContractCategory())) {
|
||||
ContractManage recorded = contractManageService.getById(waybill.getCarrierContractId());
|
||||
if (recorded == null || !"承运商合同".equals(recorded.getContractCategory())) {
|
||||
throw new ServiceException("运单记录的承运商合同不存在或合同类别不正确");
|
||||
}
|
||||
return contract;
|
||||
if (carrierName == null || matchesWaybillCarrier(recorded, waybill)) {
|
||||
return recorded;
|
||||
}
|
||||
}
|
||||
return findCarrierContract(waybill);
|
||||
ContractManage matched = findCarrierContract(waybill);
|
||||
if (matched != null && matchesWaybillCarrier(matched, waybill)) {
|
||||
return matched;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchesWaybillCarrier(ContractManage contract, Waybill waybill) {
|
||||
if (contract == null || waybill == null || !"承运商合同".equals(contract.getContractCategory())) {
|
||||
return false;
|
||||
}
|
||||
if (waybill.getProjectId() != null && contract.getProjectId() != null
|
||||
&& !Objects.equals(waybill.getProjectId(), contract.getProjectId())) {
|
||||
return false;
|
||||
}
|
||||
String carrierName = trimToNull(waybill.getCarrierName());
|
||||
String partyB = trimToNull(contract.getPartyB());
|
||||
return carrierName != null && carrierName.equals(partyB);
|
||||
}
|
||||
|
||||
private ContractManage findCarrierContract(Waybill waybill) {
|
||||
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId())
|
||||
|| Func.isEmpty(waybill.getCarrierName())) {
|
||||
String carrierName = trimToNull(waybill.getCarrierName());
|
||||
if (waybill.getProjectId() == null || Func.isEmpty(waybill.getContractId()) || carrierName == null) {
|
||||
return null;
|
||||
}
|
||||
// 承运商合同甲方应与运单绑定的客户合同甲方一致,不能直接使用运单的客户名称拼接字段。
|
||||
// 承运商合同甲方应与运单绑定的客户合同甲方一致,乙方必须是运单选择的承运商。
|
||||
ContractManage customerContract = contractManageService.getById(waybill.getContractId());
|
||||
if (customerContract == null || Func.isEmpty(customerContract.getPartyA())) {
|
||||
return null;
|
||||
@@ -1142,13 +1189,38 @@ public class ReceivablePayableDetailServiceImpl
|
||||
.eq(ContractManage::getIsDeleted, 0)
|
||||
.eq(ContractManage::getProjectId, waybill.getProjectId())
|
||||
.eq(ContractManage::getPartyA, customerContract.getPartyA())
|
||||
.eq(ContractManage::getPartyB, waybill.getCarrierName())
|
||||
.eq(ContractManage::getPartyB, carrierName)
|
||||
.eq(ContractManage::getContractCategory, "承运商合同")
|
||||
.and(wrapper -> wrapper.isNull(ContractManage::getContractStage)
|
||||
.or().ne(ContractManage::getContractStage, "terminated"))
|
||||
.orderByDesc(ContractManage::getCreateTime), false);
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = value.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应付客商取运单选择的承运商,避免写成项目内其他承运商合同的乙方。
|
||||
*/
|
||||
private String resolveCounterpartyName(Waybill waybill, ContractManage contract, String settlementType) {
|
||||
if ("payable".equals(settlementType)) {
|
||||
String carrierName = trimToNull(waybill == null ? null : waybill.getCarrierName());
|
||||
if (carrierName != null) {
|
||||
return carrierName;
|
||||
}
|
||||
return contract == null ? null : contract.getPartyB();
|
||||
}
|
||||
if (contract != null && Func.isNotEmpty(contract.getPartyA())) {
|
||||
return contract.getPartyA();
|
||||
}
|
||||
return waybill == null ? null : waybill.getCustomerName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
|
||||
rollbackFor = Exception.class)
|
||||
@@ -1201,12 +1273,24 @@ public class ReceivablePayableDetailServiceImpl
|
||||
.collect(Collectors.toMap(ContractManage::getId, contract -> contract));
|
||||
Map<Long, List<Waybill>> carrierWaybills = waybills.stream().collect(Collectors.groupingBy(
|
||||
Waybill::getCarrierContractId, LinkedHashMap::new, Collectors.toList()));
|
||||
List<Waybill> mismatchedCarrierWaybills = waybills.stream()
|
||||
.filter(waybill -> !matchesWaybillCarrier(carrierContracts.get(waybill.getCarrierContractId()), waybill))
|
||||
.toList();
|
||||
if (!mismatchedCarrierWaybills.isEmpty()) {
|
||||
generateAutomaticWaybillDetails(mismatchedCarrierWaybills, false,
|
||||
this::resolveCarrierContract, currentUserId, generateTime);
|
||||
}
|
||||
for (Map.Entry<Long, List<Waybill>> entry : carrierWaybills.entrySet()) {
|
||||
ContractManage carrierContract = carrierContracts.get(entry.getKey());
|
||||
if (carrierContract == null || !"承运商合同".equals(carrierContract.getContractCategory())) {
|
||||
throw new ServiceException("总单【" + masterOrder.getMasterNo() + "】记录的承运商合同不存在或合同类别不正确");
|
||||
}
|
||||
List<Waybill> currentCarrierWaybills = entry.getValue();
|
||||
List<Waybill> currentCarrierWaybills = entry.getValue().stream()
|
||||
.filter(waybill -> matchesWaybillCarrier(carrierContract, waybill))
|
||||
.toList();
|
||||
if (currentCarrierWaybills.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
generateAutomaticDetail(carrierContract, "payable",
|
||||
() -> existsByMasterOrderContract(masterOrder.getMasterNo(), "payable", carrierContract.getId()),
|
||||
currentCarrierWaybills,
|
||||
@@ -1635,9 +1719,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setDeptId(contract == null ? waybill.getDeptId() : contract.getOrganizationId());
|
||||
detail.setDeptName(contract == null ? waybill.getDeptName() : contract.getOrganizationName());
|
||||
detail.setFeeDate(waybill.getEndDate() == null ? LocalDate.now() : waybill.getEndDate());
|
||||
detail.setCustomerName(contract == null
|
||||
? ("payable".equals(settlementType) ? waybill.getCarrierName() : waybill.getCustomerName())
|
||||
: ("payable".equals(settlementType) ? contract.getPartyB() : contract.getPartyA()));
|
||||
detail.setCustomerName(resolveCounterpartyName(waybill, contract, settlementType));
|
||||
detail.setContractId(contract == null ? waybill.getContractId() : contract.getId());
|
||||
detail.setContractNo(contract == null ? null : contract.getContractNo());
|
||||
detail.setContractName(contract == null ? waybill.getContractName() : contract.getContractName());
|
||||
@@ -1652,8 +1734,8 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setArrivalContact(waybill.getArrivalContact());
|
||||
detail.setArrivalPhone(waybill.getArrivalPhone());
|
||||
detail.setTransportType(waybill.getTransportType());
|
||||
detail.setCargoName(waybill.getCargoName());
|
||||
detail.setCargoType(waybill.getCargoType());
|
||||
detail.setCargoName(joinDistinctGoods(waybill, "cargoName", waybill.getCargoName()));
|
||||
detail.setCargoType(joinDistinctGoods(waybill, "cargoType", waybill.getCargoType()));
|
||||
detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill));
|
||||
detail.setQuantityUnit(waybill.getQuantityUnit());
|
||||
detail.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
|
||||
@@ -1668,6 +1750,21 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return detail;
|
||||
}
|
||||
|
||||
/** 一单多货时主表只保留首行货物,应付明细改记全部货物名称、货物类型。 */
|
||||
private String joinDistinctGoods(Waybill waybill, String field, String fallback) {
|
||||
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||
for (Map<String, Object> goods : parseList(waybill.getGoodsJson())) {
|
||||
String value = stringValue(goods, field, "");
|
||||
if (Func.isNotEmpty(value)) {
|
||||
values.add(value.trim());
|
||||
}
|
||||
}
|
||||
if (values.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
return String.join(",", values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多货物运单优先按 goodsJson 数量合计作为运输总量,避免主表 quantity 只落了首行。
|
||||
*/
|
||||
|
||||
+56
-3
@@ -1233,14 +1233,67 @@ public class TransportReconciliationServiceImpl
|
||||
private List<TransportReconciliationInternal> assertAllMatched(TransportReconciliation bill) {
|
||||
List<TransportReconciliationInternal> internals = internalRows(bill.getId());
|
||||
List<TransportReconciliationExternal> externals = externalRows(bill.getId());
|
||||
if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size()
|
||||
|| internals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchResult()))
|
||||
|| externals.stream().anyMatch(item -> !MATCHED.equals(item.getMatchStatus()) || Boolean.TRUE.equals(item.getSuspectedDuplicate()))) {
|
||||
// 调整账单后旧的疑似重复标记可能已失效,按当前内容重算,避免已一一匹配仍被拦截
|
||||
syncSuspectedDuplicates(externals);
|
||||
if (!oneToOneMatched(internals, externals)) {
|
||||
throw new ServiceException("所有内外部账单明细必须一一匹配且不存在疑似重复");
|
||||
}
|
||||
return internals;
|
||||
}
|
||||
|
||||
private void syncSuspectedDuplicates(List<TransportReconciliationExternal> externals) {
|
||||
Map<String, List<TransportReconciliationExternal>> groups = externals.stream()
|
||||
.collect(Collectors.groupingBy(this::duplicateKey));
|
||||
Set<Long> duplicateIds = new HashSet<>();
|
||||
for (List<TransportReconciliationExternal> group : groups.values()) {
|
||||
if (group.size() > 1) {
|
||||
group.stream().map(TransportReconciliationExternal::getId).forEach(duplicateIds::add);
|
||||
}
|
||||
}
|
||||
for (TransportReconciliationExternal external : externals) {
|
||||
boolean duplicate = duplicateIds.contains(external.getId());
|
||||
String matchStatus = external.getMatchStatus();
|
||||
if (!duplicate && DUPLICATE.equals(matchStatus) && external.getMatchedInternalId() != null) {
|
||||
matchStatus = MATCHED;
|
||||
}
|
||||
if (Boolean.TRUE.equals(external.getSuspectedDuplicate()) == duplicate
|
||||
&& Objects.equals(matchStatus, external.getMatchStatus())) {
|
||||
continue;
|
||||
}
|
||||
external.setSuspectedDuplicate(duplicate);
|
||||
external.setMatchStatus(matchStatus);
|
||||
externalMapper.update(null, Wrappers.<TransportReconciliationExternal>lambdaUpdate()
|
||||
.eq(TransportReconciliationExternal::getId, external.getId())
|
||||
.set(TransportReconciliationExternal::getSuspectedDuplicate, duplicate)
|
||||
.set(TransportReconciliationExternal::getMatchStatus, matchStatus));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean oneToOneMatched(List<TransportReconciliationInternal> internals,
|
||||
List<TransportReconciliationExternal> externals) {
|
||||
if (internals.isEmpty() || externals.isEmpty() || internals.size() != externals.size()) {
|
||||
return false;
|
||||
}
|
||||
if (externals.stream().anyMatch(item -> Boolean.TRUE.equals(item.getSuspectedDuplicate()))) {
|
||||
return false;
|
||||
}
|
||||
Map<Long, TransportReconciliationExternal> externalMap = externals.stream()
|
||||
.collect(Collectors.toMap(TransportReconciliationExternal::getId, Function.identity(), (left, right) -> left));
|
||||
Set<Long> usedExternalIds = new HashSet<>();
|
||||
for (TransportReconciliationInternal internal : internals) {
|
||||
if (!MATCHED.equals(internal.getMatchResult()) || internal.getMatchedExternalId() == null
|
||||
|| !usedExternalIds.add(internal.getMatchedExternalId())) {
|
||||
return false;
|
||||
}
|
||||
TransportReconciliationExternal external = externalMap.get(internal.getMatchedExternalId());
|
||||
if (external == null || !MATCHED.equals(external.getMatchStatus())
|
||||
|| !Objects.equals(external.getMatchedInternalId(), internal.getId())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return usedExternalIds.size() == externals.size();
|
||||
}
|
||||
|
||||
private void refreshStats(Long id) {
|
||||
TransportReconciliation bill = existing(id);
|
||||
List<TransportReconciliationInternal> internals = internalRows(id);
|
||||
|
||||
+10
-2
@@ -385,9 +385,17 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
public BusinessRemoveResultVO removeBatches(String ids) {
|
||||
List<Long> idList = Func.toLongList(ids);
|
||||
if (Func.isEmpty(idList)) throw new ServiceException("请选择需要删除的运单批次");
|
||||
List<WaybillImportBatch> batches = listByIds(idList).stream()
|
||||
.filter(batch -> !Objects.equals(batch.getIsDeleted(), 1))
|
||||
.toList();
|
||||
// 与前端一致:仅草稿可删;选中含已完成/进行中时整批拒绝
|
||||
boolean hasNonDraft = batches.stream()
|
||||
.anyMatch(batch -> !STATUS_DRAFT.equals(batch.getImportStatus()));
|
||||
if (hasNonDraft) {
|
||||
throw new ServiceException("已完成运单不可删除");
|
||||
}
|
||||
BusinessRemoveResultVO result = new BusinessRemoveResultVO();
|
||||
for (WaybillImportBatch batch : listByIds(idList)) {
|
||||
if (Objects.equals(batch.getIsDeleted(), 1)) continue;
|
||||
for (WaybillImportBatch batch : batches) {
|
||||
List<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getImportBatchId, batch.getId()).eq(Waybill::getIsDeleted, 0));
|
||||
if (Func.isNotEmpty(waybills)) {
|
||||
|
||||
+2
-1
@@ -1495,7 +1495,8 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean complete(Long id) {
|
||||
return doComplete(loadEditable(id, true));
|
||||
// 暂时取消组织校验:历史运单 deptId 可能为 -1 / 与当前组织不一致,完成时不再拦截
|
||||
return doComplete(loadEditable(id, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,7 +44,9 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_change_record` (
|
||||
`update_user` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int(11) DEFAULT '1', `is_deleted` int(11) DEFAULT '0',
|
||||
`formal_settlement_id` bigint(20) NOT NULL COMMENT '正式结算单ID', `change_type` varchar(50) NOT NULL COMMENT '变更类型',
|
||||
`line_no` int(11) DEFAULT NULL COMMENT '行号', `operation_type` varchar(30) NOT NULL COMMENT '操作类型',
|
||||
`change_content` text NOT NULL COMMENT '变更内容', `change_reason` varchar(200) DEFAULT NULL COMMENT '变更原因',
|
||||
`change_content` text NOT NULL COMMENT '变更内容',
|
||||
`before_data` longtext COMMENT '变更前数据JSON', `after_data` longtext COMMENT '变更后数据JSON',
|
||||
`change_reason` varchar(200) DEFAULT NULL COMMENT '变更原因',
|
||||
`operator_name` varchar(100) DEFAULT NULL COMMENT '操作人', `change_time` datetime NOT NULL COMMENT '变更时间',
|
||||
PRIMARY KEY (`id`), KEY `idx_formal_settlement_change_bill` (`formal_settlement_id`),
|
||||
KEY `idx_formal_settlement_change_time` (`change_time`)
|
||||
@@ -123,17 +125,29 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_payment` (
|
||||
) 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)
|
||||
(2090000000000001040,2090000000000001000,'payment_settlement','付款结算单','payment_settlement','/settlement/payment-settlement','iconfont icon-caidanguanli',4,1,0,1,NULL,'',0),
|
||||
(2090000000000001041,2090000000000001040,'payment_settlement_view','查看','payment_settlement_view','','',1,2,0,1,NULL,'',0),
|
||||
(2090000000000001042,2090000000000001040,'payment_settlement_add','新增','payment_settlement_add','','',2,2,0,1,NULL,'',0),
|
||||
(2090000000000001043,2090000000000001040,'payment_settlement_edit','编辑','payment_settlement_edit','','',3,2,0,1,NULL,'',0),
|
||||
(2090000000000001044,2090000000000001040,'payment_settlement_delete','删除','payment_settlement_delete','','',4,2,0,1,NULL,'',0),
|
||||
(2090000000000001045,2090000000000001040,'payment_settlement_submit','提交审批','payment_settlement_submit','','',5,2,0,1,NULL,'',0),
|
||||
(2090000000000001046,2090000000000001040,'payment_settlement_approve','审批','payment_settlement_approve','','',6,2,0,1,NULL,'',0),
|
||||
(2090000000000001047,2090000000000001040,'payment_settlement_sync','同步金蝶','payment_settlement_sync','','',7,2,0,1,NULL,'',0),
|
||||
(2090000000000001048,2090000000000001040,'payment_settlement_print','打印','payment_settlement_print','','',8,2,0,1,NULL,'',0),
|
||||
(2090000000000001049,2090000000000001040,'payment_settlement_export','导出','payment_settlement_export','','',9,2,0,1,NULL,'',0),
|
||||
(2090000000000001050,2090000000000001040,'payment_settlement_void','作废','payment_settlement_void','','',10,2,0,1,NULL,'',0),
|
||||
(2090000000000001051,2090000000000001040,'payment_settlement_adjust','明细调整','payment_settlement_adjust','','',11,2,0,1,NULL,'',0),
|
||||
(2090000000000001052,2090000000000001040,'payment_settlement_payment','付款申请','payment_settlement_payment','','',12,2,0,1,NULL,'',0),
|
||||
(2090000000000002100,2090000000000001000,'receipt_settlement','收款结算单','receipt_settlement','/settlement/receipt-settlement','iconfont icon-caidanguanli',5,1,0,1,NULL,'',0),
|
||||
(2090000000000002101,2090000000000002100,'receipt_settlement_view','查看','receipt_settlement_view','','',1,2,0,1,NULL,'',0),
|
||||
(2090000000000002102,2090000000000002100,'receipt_settlement_add','新增','receipt_settlement_add','','',2,2,0,1,NULL,'',0),
|
||||
(2090000000000002103,2090000000000002100,'receipt_settlement_edit','编辑','receipt_settlement_edit','','',3,2,0,1,NULL,'',0),
|
||||
(2090000000000002104,2090000000000002100,'receipt_settlement_delete','删除','receipt_settlement_delete','','',4,2,0,1,NULL,'',0),
|
||||
(2090000000000002105,2090000000000002100,'receipt_settlement_submit','提交审批','receipt_settlement_submit','','',5,2,0,1,NULL,'',0),
|
||||
(2090000000000002106,2090000000000002100,'receipt_settlement_approve','审批','receipt_settlement_approve','','',6,2,0,1,NULL,'',0),
|
||||
(2090000000000002107,2090000000000002100,'receipt_settlement_sync','同步金蝶','receipt_settlement_sync','','',7,2,0,1,NULL,'',0),
|
||||
(2090000000000002108,2090000000000002100,'receipt_settlement_print','打印','receipt_settlement_print','','',8,2,0,1,NULL,'',0),
|
||||
(2090000000000002109,2090000000000002100,'receipt_settlement_export','导出','receipt_settlement_export','','',9,2,0,1,NULL,'',0),
|
||||
(2090000000000002110,2090000000000002100,'receipt_settlement_void','作废','receipt_settlement_void','','',10,2,0,1,NULL,'',0),
|
||||
(2090000000000002111,2090000000000002100,'receipt_settlement_adjust','明细调整','receipt_settlement_adjust','','',11,2,0,1,NULL,'',0)
|
||||
ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`path`=VALUES(`path`),`is_deleted`=0;
|
||||
|
||||
@@ -14,6 +14,8 @@ CREATE TABLE IF NOT EXISTS `blade_formal_settlement_change_record` (
|
||||
`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 '变更内容',
|
||||
`before_data` longtext COLLATE utf8mb4_general_ci COMMENT '变更前数据JSON',
|
||||
`after_data` longtext COLLATE utf8mb4_general_ci COMMENT '变更后数据JSON',
|
||||
`change_reason` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因',
|
||||
`operator_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人',
|
||||
`change_time` datetime NOT NULL COMMENT '变更时间',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
-- 正式结算变更记录补充前后值明细,与预结算变更记录口径一致。
|
||||
SET @before_data_exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_formal_settlement_change_record'
|
||||
AND COLUMN_NAME = 'before_data'
|
||||
);
|
||||
SET @before_data_sql := IF(
|
||||
@before_data_exists = 0,
|
||||
'ALTER TABLE `blade_formal_settlement_change_record` ADD COLUMN `before_data` longtext COLLATE utf8mb4_general_ci COMMENT ''变更前数据JSON'' AFTER `change_content`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE before_data_stmt FROM @before_data_sql;
|
||||
EXECUTE before_data_stmt;
|
||||
DEALLOCATE PREPARE before_data_stmt;
|
||||
|
||||
SET @after_data_exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'blade_formal_settlement_change_record'
|
||||
AND COLUMN_NAME = 'after_data'
|
||||
);
|
||||
SET @after_data_sql := IF(
|
||||
@after_data_exists = 0,
|
||||
'ALTER TABLE `blade_formal_settlement_change_record` ADD COLUMN `after_data` longtext COLLATE utf8mb4_general_ci COMMENT ''变更后数据JSON'' AFTER `before_data`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE after_data_stmt FROM @after_data_sql;
|
||||
EXECUTE after_data_stmt;
|
||||
DEALLOCATE PREPARE after_data_stmt;
|
||||
@@ -200,7 +200,7 @@ CREATE TABLE IF NOT EXISTS `blade_pre_settlement_change_record` (
|
||||
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),
|
||||
(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),
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
-- 结算菜单调整:
|
||||
-- 1)预结算 → 预付结算单
|
||||
-- 2)正式结算单拆分为:付款结算单(应付)、收款结算单(应收)
|
||||
-- 执行后需重新登录;已授权「正式结算单/付款结算单」的角色会同步授权「收款结算单」。
|
||||
|
||||
-- ① 预结算改名
|
||||
UPDATE `blade_menu`
|
||||
SET `name` = '预付结算单'
|
||||
WHERE `code` = 'pre_settlement'
|
||||
AND `is_deleted` = 0;
|
||||
|
||||
-- ② 原正式结算单菜单改为「付款结算单」(保留原菜单 id,角色授权不丢)
|
||||
UPDATE `blade_menu`
|
||||
SET `code` = 'payment_settlement',
|
||||
`name` = '付款结算单',
|
||||
`alias` = 'payment_settlement',
|
||||
`path` = '/settlement/payment-settlement',
|
||||
`is_deleted` = 0
|
||||
WHERE `code` IN ('formal_settlement', 'payment_settlement')
|
||||
AND `category` = 1
|
||||
AND `is_deleted` = 0;
|
||||
|
||||
-- 付款结算单按钮权限码:formal_settlement_* → payment_settlement_*
|
||||
UPDATE `blade_menu` child
|
||||
INNER JOIN `blade_menu` parent
|
||||
ON parent.`id` = child.`parent_id`
|
||||
AND parent.`code` = 'payment_settlement'
|
||||
AND parent.`is_deleted` = 0
|
||||
SET child.`code` = REPLACE(child.`code`, 'formal_settlement_', 'payment_settlement_'),
|
||||
child.`alias` = REPLACE(IFNULL(child.`alias`, child.`code`), 'formal_settlement_', 'payment_settlement_'),
|
||||
child.`is_deleted` = 0
|
||||
WHERE child.`category` = 2
|
||||
AND child.`code` LIKE 'formal_settlement_%';
|
||||
|
||||
-- ③ 新增「收款结算单」菜单
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT
|
||||
2090000000000002100,
|
||||
parent.`parent_id`,
|
||||
'receipt_settlement',
|
||||
'收款结算单',
|
||||
'receipt_settlement',
|
||||
'/settlement/receipt-settlement',
|
||||
IFNULL(parent.`source`, 'iconfont icon-caidanguanli'),
|
||||
IFNULL(parent.`sort`, 4) + 1,
|
||||
1, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` parent
|
||||
WHERE parent.`code` = 'payment_settlement'
|
||||
AND parent.`is_deleted` = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `blade_menu` x WHERE x.`code` = 'receipt_settlement' AND x.`is_deleted` = 0
|
||||
)
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002101, p.id, 'receipt_settlement_view', '查看', 'receipt_settlement_view', '', '', 1, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_view' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002102, p.id, 'receipt_settlement_add', '新增', 'receipt_settlement_add', '', '', 2, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_add' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002103, p.id, 'receipt_settlement_edit', '编辑', 'receipt_settlement_edit', '', '', 3, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_edit' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002104, p.id, 'receipt_settlement_delete', '删除', 'receipt_settlement_delete', '', '', 4, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_delete' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002105, p.id, 'receipt_settlement_submit', '提交审批', 'receipt_settlement_submit', '', '', 5, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_submit' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002106, p.id, 'receipt_settlement_approve', '审批', 'receipt_settlement_approve', '', '', 6, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_approve' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002107, p.id, 'receipt_settlement_sync', '同步金蝶', 'receipt_settlement_sync', '', '', 7, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_sync' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002108, p.id, 'receipt_settlement_print', '打印', 'receipt_settlement_print', '', '', 8, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_print' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002109, p.id, 'receipt_settlement_export', '导出', 'receipt_settlement_export', '', '', 9, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_export' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002110, p.id, 'receipt_settlement_void', '作废', 'receipt_settlement_void', '', '', 10, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_void' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
INSERT INTO `blade_menu`
|
||||
(`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
|
||||
SELECT 2090000000000002111, p.id, 'receipt_settlement_adjust', '明细调整', 'receipt_settlement_adjust', '', '', 11, 2, 0, 1, NULL, '', 0
|
||||
FROM `blade_menu` p WHERE p.code = 'receipt_settlement' AND p.is_deleted = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM `blade_menu` x WHERE x.code = 'receipt_settlement_adjust' AND x.is_deleted = 0)
|
||||
LIMIT 1;
|
||||
|
||||
-- ④ 角色授权:持有付款结算单的角色同步收款结算单(含按钮)
|
||||
INSERT INTO `blade_role_menu` (`id`, `menu_id`, `role_id`)
|
||||
SELECT
|
||||
2091200000000000000 + CAST(rm.`role_id` AS UNSIGNED) % 100000,
|
||||
receipt.`id`,
|
||||
rm.`role_id`
|
||||
FROM `blade_role_menu` rm
|
||||
JOIN `blade_menu` payment ON payment.`id` = rm.`menu_id`
|
||||
AND payment.`code` = 'payment_settlement'
|
||||
AND payment.`is_deleted` = 0
|
||||
JOIN `blade_menu` receipt ON receipt.`code` = 'receipt_settlement'
|
||||
AND receipt.`is_deleted` = 0
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `blade_role_menu` x
|
||||
WHERE x.`menu_id` = receipt.`id` AND x.`role_id` = rm.`role_id`
|
||||
);
|
||||
|
||||
INSERT INTO `blade_role_menu` (`id`, `menu_id`, `role_id`)
|
||||
SELECT
|
||||
2091300000000000000
|
||||
+ (CAST(rm.`role_id` AS UNSIGNED) % 100000) * 20
|
||||
+ (CAST(receipt_btn.`id` AS UNSIGNED) % 20),
|
||||
receipt_btn.`id`,
|
||||
rm.`role_id`
|
||||
FROM `blade_role_menu` rm
|
||||
JOIN `blade_menu` payment_btn ON payment_btn.`id` = rm.`menu_id`
|
||||
AND payment_btn.`code` LIKE 'payment_settlement_%'
|
||||
AND payment_btn.`is_deleted` = 0
|
||||
JOIN `blade_menu` receipt_btn
|
||||
ON receipt_btn.`code` = REPLACE(payment_btn.`code`, 'payment_settlement_', 'receipt_settlement_')
|
||||
AND receipt_btn.`is_deleted` = 0
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `blade_role_menu` x
|
||||
WHERE x.`menu_id` = receipt_btn.`id` AND x.`role_id` = rm.`role_id`
|
||||
);
|
||||
|
||||
-- ⑤ 残留旧正式结算菜单隐藏
|
||||
UPDATE `blade_menu`
|
||||
SET `is_deleted` = 1
|
||||
WHERE `code` = 'formal_settlement'
|
||||
AND `category` = 1;
|
||||
Reference in New Issue
Block a user