Merge branch 'master' into feature/20261104-bfi-project-contract-ref-v2

This commit is contained in:
刘泉佳
2026-11-05 00:23:00 +08:00
21 changed files with 665 additions and 86 deletions
@@ -139,9 +139,9 @@ public class Dept extends TenantEntity {
private String deptCode; private String deptCode;
/** /**
* 金蝶部门代码 * 金蝶公司代码(平台公司维护;字段名 kingdeeDeptCode 为历史遗留)
*/ */
@Schema(description = "金蝶部门代码") @Schema(description = "金蝶公司代码")
private String kingdeeDeptCode; private String kingdeeDeptCode;
/** /**
@@ -126,6 +126,10 @@ public class CustomerArchive extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDate businessEndDate; private LocalDate businessEndDate;
@Schema(description = "下次评定日期")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDate nextEvaluateDate;
@Schema(description = "注册资金(万元)") @Schema(description = "注册资金(万元)")
@TableField(updateStrategy = FieldStrategy.ALWAYS) @TableField(updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal registeredCapital; private BigDecimal registeredCapital;
@@ -56,9 +56,15 @@ public class CustomerCreditScore extends TenantEntity {
@Schema(description = "评分量化表ID") @Schema(description = "评分量化表ID")
private Long quantificationId; private Long quantificationId;
@Schema(description = "评估方式:online线上评估,offline线下评估")
private String evaluateMode;
@Schema(description = "评分日期") @Schema(description = "评分日期")
private LocalDate scoreDate; private LocalDate scoreDate;
@Schema(description = "下次评定日期")
private LocalDate nextEvaluateDate;
@Schema(description = "自评得分") @Schema(description = "自评得分")
private BigDecimal selfScore; private BigDecimal selfScore;
@@ -68,7 +68,7 @@ public class ContractManageVO extends ContractManage {
private String effectiveType; private String effectiveType;
@TableField(exist = false) @TableField(exist = false)
@Schema(description = "客商(匹配甲/乙/丙方)") @Schema(description = "客商筛选:按甲方/乙方名称模糊匹配(丙方未落库,不参与)")
private String partyName; private String partyName;
@TableField(exist = false) @TableField(exist = false)
@@ -106,6 +106,10 @@ public class CustomerArchiveVO extends CustomerArchive {
@Schema(description = "已付金额合计(元)") @Schema(description = "已付金额合计(元)")
private BigDecimal usedFundLimit; private BigDecimal usedFundLimit;
@TableField(exist = false)
@Schema(description = "到期筛查:all全部/soon三十天内到期/expired已过期/license_soon执照即将到期/evaluate_soon评定即将到期")
private String expireStatus;
@TableField(exist = false) @TableField(exist = false)
@Schema(description = "客商风险筛选值:blacklist黑名单/high高风险/middle中风险/low低风险") @Schema(description = "客商风险筛选值:blacklist黑名单/high高风险/middle中风险/low低风险")
private String customerRisk; private String customerRisk;
@@ -112,9 +112,15 @@ public class BusinessDeptConfigServiceImpl extends BaseServiceImpl<BusinessDeptC
businessDeptConfig.setPlatformCompanyName(platformCompany.getDeptName()); businessDeptConfig.setPlatformCompanyName(platformCompany.getDeptName());
businessDeptConfig.setBusinessDeptName(businessDept.getDeptName()); businessDeptConfig.setBusinessDeptName(businessDept.getDeptName());
businessDeptConfig.setContractCodePrefix(trimToEmpty(businessDeptConfig.getContractCodePrefix())); businessDeptConfig.setContractCodePrefix(trimToEmpty(businessDeptConfig.getContractCodePrefix()));
businessDeptConfig.setKingdeeCompanyCode(trimToNull(businessDeptConfig.getKingdeeCompanyCode()));
businessDeptConfig.setKingdeeDeptCode(trimToNull(businessDeptConfig.getKingdeeDeptCode())); businessDeptConfig.setKingdeeDeptCode(trimToNull(businessDeptConfig.getKingdeeDeptCode()));
businessDeptConfig.setRemark(trimToNull(businessDeptConfig.getRemark())); businessDeptConfig.setRemark(trimToNull(businessDeptConfig.getRemark()));
// 金蝶公司代码始终关联平台公司组织中配置的金蝶代码(业务部门配置仅展示)
businessDeptConfig.setKingdeeCompanyCode(trimToNull(platformCompany.getKingdeeDeptCode()));
// Avue number 空值常传 -1,未填时落库 null,禁止保存哨兵值
Integer initialSerial = businessDeptConfig.getInitialSerial();
if (initialSerial != null && initialSerial < 1) {
businessDeptConfig.setInitialSerial(null);
}
if (Func.isEmpty(businessDeptConfig.getStatus())) { if (Func.isEmpty(businessDeptConfig.getStatus())) {
businessDeptConfig.setStatus(1); businessDeptConfig.setStatus(1);
} }
@@ -133,9 +139,6 @@ public class BusinessDeptConfigServiceImpl extends BaseServiceImpl<BusinessDeptC
if (businessDeptConfig.getContractCodePrefix().length() > PREFIX_MAX_LENGTH) { if (businessDeptConfig.getContractCodePrefix().length() > PREFIX_MAX_LENGTH) {
throw new ServiceException("合同编码前缀不能超过64字"); throw new ServiceException("合同编码前缀不能超过64字");
} }
if (Func.isEmpty(businessDeptConfig.getKingdeeCompanyCode())) {
throw new ServiceException("请输入金蝶公司代码");
}
if (Func.isNotEmpty(businessDeptConfig.getPlatformCompanyName()) if (Func.isNotEmpty(businessDeptConfig.getPlatformCompanyName())
&& businessDeptConfig.getPlatformCompanyName().length() > NAME_MAX_LENGTH) { && businessDeptConfig.getPlatformCompanyName().length() > NAME_MAX_LENGTH) {
throw new ServiceException("平台公司名称不能超过100字"); throw new ServiceException("平台公司名称不能超过100字");
@@ -144,7 +147,8 @@ public class BusinessDeptConfigServiceImpl extends BaseServiceImpl<BusinessDeptC
&& businessDeptConfig.getBusinessDeptName().length() > NAME_MAX_LENGTH) { && businessDeptConfig.getBusinessDeptName().length() > NAME_MAX_LENGTH) {
throw new ServiceException("业务部门名称不能超过100字"); throw new ServiceException("业务部门名称不能超过100字");
} }
if (businessDeptConfig.getKingdeeCompanyCode().length() > CODE_MAX_LENGTH) { if (Func.isNotEmpty(businessDeptConfig.getKingdeeCompanyCode())
&& businessDeptConfig.getKingdeeCompanyCode().length() > CODE_MAX_LENGTH) {
throw new ServiceException("金蝶公司代码不能超过64字"); throw new ServiceException("金蝶公司代码不能超过64字");
} }
if (Func.isNotEmpty(businessDeptConfig.getKingdeeDeptCode()) if (Func.isNotEmpty(businessDeptConfig.getKingdeeDeptCode())
@@ -32,6 +32,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Condition;
@@ -44,6 +46,7 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.transport.excel.CustomerArchiveExcel; import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.transport.excel.CustomerCreditScoreExcel;
import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.vo.CustomerArchiveVO; import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
@@ -55,6 +58,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
@@ -239,6 +243,52 @@ public class CustomerArchiveController extends BladeController {
return R.data(customerArchiveService.buildScoreTemplate(quantificationId)); return R.data(customerArchiveService.buildScoreTemplate(quantificationId));
} }
/**
* 线下评分 Excel 模板下载
*/
@GetMapping("/score-excel-template")
@ApiOperationSupport(order = 10)
@Operation(summary = "线下评分Excel模板", description = "按评分量化表ID导出可填写模板")
public void scoreExcelTemplate(@RequestParam Long quantificationId, HttpServletResponse response) {
List<CustomerCreditScoreExcel> list = customerArchiveService.buildScoreExcelTemplate(quantificationId);
ExcelUtil.export(response, "客商评分明细模板" + DateUtil.time(), "评分明细", list, CustomerCreditScoreExcel.class);
}
/**
* 线下评分 Excel 导入(解析回填评分明细,失败导出失败明细)
*/
@PostMapping("/score-import")
@ApiOperationSupport(order = 10)
@Operation(summary = "线下评分Excel导入", description = "解析回填评分明细;失败时导出导入失败明细")
public R<CustomerCreditScoreVO> scoreImport(@RequestParam Long quantificationId,
MultipartFile file,
HttpServletResponse response) {
if (file == null || file.isEmpty()) {
return R.fail("上传文件不能为空");
}
String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase();
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
return R.fail("请上传 .xls,.xlsx 标准格式文件");
}
try {
CustomerCreditScoreVO score = customerArchiveService.importScoreExcel(
quantificationId, ExcelUtil.read(file, CustomerCreditScoreExcel.class));
return R.data(score);
} catch (ImportFailureException exception) {
ImportFailureExcelUtil.export(response,
"客商评分导入失败明细" + DateUtil.time(), "导入失败明细",
exception.getFailureList(), CustomerCreditScoreExcel.class);
return null;
} catch (Exception readException) {
if (ImportFailureExcelUtil.exportReadFailure(
response, "客商评分导入失败明细" + DateUtil.time(), "导入失败明细",
file, CustomerCreditScoreExcel.class, readException)) {
return null;
}
throw readException;
}
}
/** /**
* 导出客商档案 * 导出客商档案
*/ */
@@ -0,0 +1,70 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.excel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.write.style.ContentRowHeight;
import cn.idev.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 客商线下评分 Excel(模板下载与导入共用)
*
* @author Chill
*/
@Data
@ColumnWidth(24)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class CustomerCreditScoreExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty("分类")
private String categoryName;
@ExcelProperty("*评分项目")
private String itemName;
@ExcelProperty("选项类型")
private String optionTypeName;
@ExcelProperty("可选选项说明")
private String optionHint;
@ExcelProperty("*填写值")
private String fillValue;
@ExcelProperty("自评得分")
private String selfScore;
@ExcelIgnore
private String errorMessage;
}
@@ -35,6 +35,7 @@
<result column="business_term_type" property="businessTermType"/> <result column="business_term_type" property="businessTermType"/>
<result column="business_start_date" property="businessStartDate"/> <result column="business_start_date" property="businessStartDate"/>
<result column="business_end_date" property="businessEndDate"/> <result column="business_end_date" property="businessEndDate"/>
<result column="next_evaluate_date" property="nextEvaluateDate"/>
<result column="registered_capital" property="registeredCapital"/> <result column="registered_capital" property="registeredCapital"/>
<result column="paid_in_capital" property="paidInCapital"/> <result column="paid_in_capital" property="paidInCapital"/>
<result column="principal" property="principal"/> <result column="principal" property="principal"/>
@@ -95,6 +96,7 @@
business_term_type, business_term_type,
business_start_date, business_start_date,
business_end_date, business_end_date,
next_evaluate_date,
registered_capital, registered_capital,
paid_in_capital, paid_in_capital,
principal, principal,
@@ -198,6 +200,38 @@
</otherwise> </otherwise>
</choose> </choose>
</if> </if>
<!-- 到期筛查:营业执照截止日 / 下次评定日期 -->
<if test="customer.expireStatus != null and customer.expireStatus != '' and customer.expireStatus != 'all'">
<choose>
<when test="customer.expireStatus == 'license_soon'">
AND IFNULL(business_term_type, '') &lt;&gt; '长期'
AND business_end_date IS NOT NULL
AND business_end_date &lt;= DATE_ADD(CURDATE(), INTERVAL 30 DAY)
</when>
<when test="customer.expireStatus == 'evaluate_soon'">
AND next_evaluate_date IS NOT NULL
AND next_evaluate_date &lt;= DATE_ADD(CURDATE(), INTERVAL 30 DAY)
</when>
<when test="customer.expireStatus == 'expired'">
AND (
(IFNULL(business_term_type, '') &lt;&gt; '长期'
AND business_end_date IS NOT NULL
AND business_end_date &lt; CURDATE())
OR (next_evaluate_date IS NOT NULL AND next_evaluate_date &lt; CURDATE())
)
</when>
<otherwise>
<!-- soon:任一字段 30 天内到期(含当天) -->
AND (
(IFNULL(business_term_type, '') &lt;&gt; '长期'
AND business_end_date IS NOT NULL
AND business_end_date &lt;= DATE_ADD(CURDATE(), INTERVAL 30 DAY))
OR (next_evaluate_date IS NOT NULL
AND next_evaluate_date &lt;= DATE_ADD(CURDATE(), INTERVAL 30 DAY))
)
</otherwise>
</choose>
</if>
ORDER BY create_time DESC ORDER BY create_time DESC
</select> </select>
@@ -41,11 +41,11 @@ import java.util.List;
public interface KingdeeReceiptFlowMapper extends BaseMapper<KingdeeReceiptFlow> { public interface KingdeeReceiptFlowMapper extends BaseMapper<KingdeeReceiptFlow> {
/** /**
* 业务部门配置中已维护的金蝶公司代码列表(去重) * 组织管理(blade_dept)中的平台公司金蝶公司代码列表(是否平台公司=是 且 已维护金蝶编码)
* *
* @return 金蝶公司代码 * @return 平台公司金蝶公司代码
*/ */
@Select("SELECT DISTINCT kingdee_company_code FROM blade_business_dept_config " @Select("SELECT kingdee_dept_code FROM blade_dept WHERE is_platform_company = 1 "
+ "WHERE kingdee_company_code IS NOT NULL AND kingdee_company_code != '' AND is_deleted = 0") + "AND kingdee_dept_code IS NOT NULL AND kingdee_dept_code != '' AND is_deleted = 0")
List<String> selectPlatformOrgNumbers(); List<String> selectPlatformOrgNumbers();
} }
@@ -252,6 +252,24 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
*/ */
CustomerCreditScoreVO buildScoreTemplate(Long quantificationId); CustomerCreditScoreVO buildScoreTemplate(Long quantificationId);
/**
* 线下评分 Excel 模板(按量化表项目生成)
*
* @param quantificationId 评分量化表ID
* @return 模板行
*/
List<org.springblade.transport.excel.CustomerCreditScoreExcel> buildScoreExcelTemplate(Long quantificationId);
/**
* 线下评分 Excel 导入:解析并回填评分明细,失败抛出 ImportFailureException
*
* @param quantificationId 评分量化表ID
* @param rows Excel 行
* @return 已回填的评分记录(含明细)
*/
CustomerCreditScoreVO importScoreExcel(Long quantificationId,
List<org.springblade.transport.excel.CustomerCreditScoreExcel> rows);
/** /**
* 导出客商档案 * 导出客商档案
* *
@@ -691,6 +691,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
queryWrapper.eq(Func.isNotEmpty(contractManage.getProjectId()), ContractManage::getProjectId, contractManage.getProjectId()) queryWrapper.eq(Func.isNotEmpty(contractManage.getProjectId()), ContractManage::getProjectId, contractManage.getProjectId())
.like(Func.isNotEmpty(contractManage.getContractNo()), ContractManage::getContractNo, contractManage.getContractNo()) .like(Func.isNotEmpty(contractManage.getContractNo()), ContractManage::getContractNo, contractManage.getContractNo())
.like(Func.isNotEmpty(contractManage.getContractName()), ContractManage::getContractName, contractManage.getContractName()) .like(Func.isNotEmpty(contractManage.getContractName()), ContractManage::getContractName, contractManage.getContractName())
// 客商即签约主体,合同表只存甲/乙方名称串(丙方未落库),命中任一方即返回
.and(Func.isNotEmpty(contractManage.getPartyName()), wrapper -> wrapper
.like(ContractManage::getPartyA, contractManage.getPartyName())
.or().like(ContractManage::getPartyB, contractManage.getPartyName()))
.like(Func.isNotEmpty(contractManage.getProjectName()) && Func.isEmpty(contractManage.getProjectId()), .like(Func.isNotEmpty(contractManage.getProjectName()) && Func.isEmpty(contractManage.getProjectId()),
ContractManage::getProjectName, contractManage.getProjectName()) ContractManage::getProjectName, contractManage.getProjectName())
.like(Func.isNotEmpty(contractManage.getOrganizationName()), ContractManage::getOrganizationName, contractManage.getOrganizationName()) .like(Func.isNotEmpty(contractManage.getOrganizationName()), ContractManage::getOrganizationName, contractManage.getOrganizationName())
@@ -906,10 +910,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
if (platformCompany == null) { if (platformCompany == null) {
throw new ServiceException("平台公司不存在"); throw new ServiceException("平台公司不存在");
} }
// 合同编号前缀改由「平台公司 + 业务部门」业务部门配置维护,不再读取组织机构 Dept.contractCodePrefix
BusinessDeptConfig config = SysCache.getBusinessDeptConfig(platformCompanyId, organizationId); BusinessDeptConfig config = SysCache.getBusinessDeptConfig(platformCompanyId, organizationId);
String prefix = config == null ? null : TransportBusinessSupport.trimToNull(config.getContractCodePrefix()); String prefix = config == null ? null : TransportBusinessSupport.trimToNull(config.getContractCodePrefix());
if (Func.isEmpty(prefix)) { if (Func.isEmpty(prefix)) {
throw new ServiceException("部门合同前缀未配置,无法自动生成,请手动输入合同编号"); throw new ServiceException("业务部门配置中未维护该平台公司与业务部门的合同编码前缀,无法自动生成,请先在业务部门配置中维护或手动输入合同编号");
} }
int year = LocalDate.now().getYear(); int year = LocalDate.now().getYear();
String head = prefix + "(" + year + ")"; String head = prefix + "(" + year + ")";
@@ -41,10 +41,13 @@ import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.common.excel.ImportFailureException;
import org.springblade.system.cache.SysCache; import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.CustomerArchiveExcel; import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.transport.excel.CustomerCreditScoreExcel;
import org.springblade.thirdparty.bfi.constant.BfiConstant; import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBankQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiBankQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
@@ -59,9 +62,6 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO;
import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil; import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.transport.config.BfiProperties; import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.mapper.CreditRatingStandardMapper; import org.springblade.transport.mapper.CreditRatingStandardMapper;
import org.springblade.transport.mapper.CreditScoreCategoryMapper; import org.springblade.transport.mapper.CreditScoreCategoryMapper;
@@ -1085,14 +1085,198 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
CreditScoreQuantification quantification = resolveQuantification(quantificationId); CreditScoreQuantification quantification = resolveQuantification(quantificationId);
CustomerCreditScoreVO score = new CustomerCreditScoreVO(); CustomerCreditScoreVO score = new CustomerCreditScoreVO();
score.setQuantificationId(quantification.getId()); score.setQuantificationId(quantification.getId());
score.setEvaluateMode("online");
score.setScoreDate(LocalDate.now()); score.setScoreDate(LocalDate.now());
score.setSelfStatus("未完成"); score.setSelfStatus("未完成");
score.setReviewStatus("未完成"); score.setReviewStatus("未完成");
score.setMaxMinusScore(quantification.getMaxMinusScore()); score.setMaxMinusScore(quantification.getMaxMinusScore());
score.setDetails(loadScoreTemplateDetails(quantification.getId())); score.setDetails(loadScoreTemplateDetails(quantification.getId()));
score.setStandards(loadStandards(quantification.getId()));
return score; return score;
} }
@Override
public List<CustomerCreditScoreExcel> buildScoreExcelTemplate(Long quantificationId) {
List<CustomerCreditScoreDetailVO> details = loadScoreTemplateDetails(quantificationId);
List<CustomerCreditScoreExcel> rows = new ArrayList<>();
for (CustomerCreditScoreDetailVO detail : details) {
CustomerCreditScoreExcel excel = new CustomerCreditScoreExcel();
excel.setCategoryName(detail.getCategoryName());
excel.setItemName(detail.getItemName());
boolean scoreType = "score".equalsIgnoreCase(detail.getOptionType());
excel.setOptionTypeName(scoreType ? "分值" : "选项");
excel.setOptionHint(scoreType
? Func.toStrWithEmpty(detail.getOptionDescription(), "请按基准数值填写实际数值")
: buildOptionHint(detail.getOptionsJson()));
excel.setFillValue("");
excel.setSelfScore("");
rows.add(excel);
}
return rows;
}
@Override
public CustomerCreditScoreVO importScoreExcel(Long quantificationId, List<CustomerCreditScoreExcel> rows) {
if (Func.isEmpty(quantificationId)) {
throw new ServiceException("请先选择评分量化表");
}
if (Func.isEmpty(rows)) {
throw new ServiceException("导入数据不能为空");
}
CustomerCreditScoreVO score = buildScoreTemplate(quantificationId);
score.setEvaluateMode("offline");
Map<String, CustomerCreditScoreDetailVO> detailByName = score.getDetails().stream()
.filter(detail -> Func.isNotEmpty(detail.getItemName()))
.collect(Collectors.toMap(detail -> detail.getItemName().trim(), detail -> detail, (left, right) -> left, LinkedHashMap::new));
boolean hasError = false;
Set<String> filledItems = new HashSet<>();
for (int index = 0; index < rows.size(); index++) {
CustomerCreditScoreExcel row = rows.get(index);
List<String> errors = new ArrayList<>();
String itemName = Func.toStrWithEmpty(row.getItemName(), "").trim();
String fillValue = Func.toStrWithEmpty(row.getFillValue(), "").trim();
if (Func.isEmpty(itemName)) {
ImportFailureExcelUtil.addValidationError(errors, true, "评分项目不能为空");
}
CustomerCreditScoreDetailVO detail = Func.isEmpty(itemName) ? null : detailByName.get(itemName);
if (Func.isNotEmpty(itemName) && detail == null) {
ImportFailureExcelUtil.addValidationError(errors, true, "评分项目[" + itemName + "]不在当前评分量化表中");
}
if (detail != null && filledItems.contains(itemName)) {
ImportFailureExcelUtil.addValidationError(errors, true, "评分项目[" + itemName + "]重复");
}
if (detail != null && Func.isEmpty(fillValue)) {
ImportFailureExcelUtil.addValidationError(errors, true, "填写值不能为空");
}
if (detail != null && Func.isNotEmpty(fillValue) && errors.isEmpty()) {
try {
applyImportedScoreDetail(detail, fillValue, Func.toStrWithEmpty(row.getSelfScore(), "").trim());
filledItems.add(itemName);
} catch (ServiceException exception) {
ImportFailureExcelUtil.addValidationError(errors, true, exception.getMessage());
}
}
if (!errors.isEmpty()) {
hasError = true;
row.setErrorMessage(ImportFailureExcelUtil.formatErrorMessage(errors));
}
}
if (hasError) {
throw new ImportFailureException(rows);
}
List<String> missing = detailByName.keySet().stream().filter(name -> !filledItems.contains(name)).toList();
if (!missing.isEmpty()) {
throw new ServiceException("评分项目未全部填写:" + String.join("、", missing));
}
return score;
}
/** 将 Excel 填写值回填到评分明细:选项型匹配选项,分值型写入 scoreInput/selfScore */
private void applyImportedScoreDetail(CustomerCreditScoreDetailVO detail, String fillValue, String selfScoreText) {
if ("score".equalsIgnoreCase(detail.getOptionType())) {
BigDecimal scoreInput;
try {
scoreInput = new BigDecimal(fillValue);
} catch (NumberFormatException exception) {
throw new ServiceException("填写值必须为数字");
}
detail.setScoreInput(scoreInput);
if (Func.isNotEmpty(selfScoreText)) {
try {
detail.setSelfScore(new BigDecimal(selfScoreText));
} catch (NumberFormatException exception) {
throw new ServiceException("自评得分必须为数字");
}
}
return;
}
Map<String, Object> matched = matchScoreOption(detail.getOptionsJson(), fillValue);
if (matched == null) {
throw new ServiceException("填写值[" + fillValue + "]不在可选选项中");
}
Object optionValue = matched.get("value");
if (optionValue == null) {
optionValue = matched.get("label");
}
if (optionValue == null) {
optionValue = matched.get("optionName");
}
detail.setSelectedOption(optionValue == null ? fillValue : String.valueOf(optionValue));
BigDecimal optionScore = resolveOptionScore(matched);
detail.setSelfScore(optionScore == null ? BigDecimal.ZERO : optionScore);
if (Func.isNotEmpty(selfScoreText)) {
try {
detail.setSelfScore(new BigDecimal(selfScoreText));
} catch (NumberFormatException exception) {
throw new ServiceException("自评得分必须为数字");
}
}
}
private Map<String, Object> matchScoreOption(String optionsJson, String fillValue) {
if (Func.isEmpty(optionsJson) || Func.isEmpty(fillValue)) {
return null;
}
try {
Object parsed = JsonUtil.parse(optionsJson, List.class);
if (!(parsed instanceof List<?> options)) {
return null;
}
for (Object option : options) {
if (!(option instanceof Map<?, ?> optionMap)) {
continue;
}
Map<String, Object> normalized = new LinkedHashMap<>();
optionMap.forEach((key, value) -> normalized.put(String.valueOf(key), value));
List<String> candidates = Stream.of(
normalized.get("label"),
normalized.get("value"),
normalized.get("optionName"))
.filter(Objects::nonNull)
.map(value -> String.valueOf(value).trim())
.filter(Func::isNotEmpty)
.toList();
if (candidates.stream().anyMatch(fillValue::equals)) {
return normalized;
}
}
} catch (Exception ignored) {
return null;
}
return null;
}
private String buildOptionHint(String optionsJson) {
if (Func.isEmpty(optionsJson)) {
return "";
}
try {
Object parsed = JsonUtil.parse(optionsJson, List.class);
if (!(parsed instanceof List<?> options)) {
return "";
}
List<String> labels = new ArrayList<>();
for (Object option : options) {
if (!(option instanceof Map<?, ?> optionMap)) {
continue;
}
Object label = optionMap.get("label");
if (label == null) {
label = optionMap.get("optionName");
}
if (label == null) {
label = optionMap.get("value");
}
if (label != null && Func.isNotEmpty(String.valueOf(label).trim())) {
labels.add(String.valueOf(label).trim());
}
}
return String.join(" / ", labels);
} catch (Exception exception) {
return "";
}
}
@Override @Override
public List<CustomerArchiveExcel> exportCustomerArchive(Wrapper<CustomerArchive> queryWrapper) { public List<CustomerArchiveExcel> exportCustomerArchive(Wrapper<CustomerArchive> queryWrapper) {
return list(queryWrapper).stream().map(this::buildExcel).toList(); return list(queryWrapper).stream().map(this::buildExcel).toList();
@@ -1250,10 +1434,32 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
if (scores.size() > 1) { if (scores.size() > 1) {
throw new ServiceException("评分记录只能有一条"); throw new ServiceException("评分记录只能有一条");
} }
// 仅客商分类含「客户」时强制评分;纯承运商不强制
if (!requiresCustomerScore(customer.getCustomerType())) {
return;
}
boolean selfCompleted = scores.size() == 1 && "已完成".equals(scores.get(0).getSelfStatus()); boolean selfCompleted = scores.size() == 1 && "已完成".equals(scores.get(0).getSelfStatus());
if (!selfCompleted) { if (!selfCompleted) {
throw new ServiceException("提交审批前必须完成自评"); throw new ServiceException("客户提交审批前必须完成评分自评");
} }
CustomerCreditScoreVO score = scores.get(0);
if (Func.isEmpty(score.getNextEvaluateDate())) {
throw new ServiceException("请填写下次评定日期");
}
if (Func.isEmpty(score.getQuantificationId())) {
throw new ServiceException("请选择评分量化表");
}
}
/** 客商分类是否包含客户(字典值「客户」或历史值 customer) */
private boolean requiresCustomerScore(String customerType) {
if (Func.isEmpty(customerType)) {
return false;
}
return Func.toStrList(customerType.replace(",", ",")).stream()
.map(String::trim)
.filter(Func::isNotEmpty)
.anyMatch(item -> "客户".equals(item) || "customer".equalsIgnoreCase(item));
} }
private void validateApproval(CustomerArchiveVO customer, boolean temporaryToFormal) { private void validateApproval(CustomerArchiveVO customer, boolean temporaryToFormal) {
@@ -1499,6 +1705,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
score.setId(scoreId); score.setId(scoreId);
score.setCustomerId(customerId); score.setCustomerId(customerId);
score.setTenantId(AuthUtil.getTenantId()); score.setTenantId(AuthUtil.getTenantId());
if (Func.isEmpty(score.getEvaluateMode())) {
score.setEvaluateMode("online");
}
score.setStatus(STATUS_ENABLED); score.setStatus(STATUS_ENABLED);
score.setIsDeleted(0); score.setIsDeleted(0);
creditScoreMapper.insert(score); creditScoreMapper.insert(score);
@@ -1593,6 +1802,10 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setMaxCreditLimit(activeScore.getMaxCreditLimit()); customer.setMaxCreditLimit(activeScore.getMaxCreditLimit());
customer.setApplyCreditLimit(activeScore.getMaxCreditLimit()); customer.setApplyCreditLimit(activeScore.getMaxCreditLimit());
} }
// 审批通过后把评分表手选的下次评定日期写回档案列表字段
if (activeScore.getNextEvaluateDate() != null) {
customer.setNextEvaluateDate(activeScore.getNextEvaluateDate());
}
} }
private List<CustomerContactVO> loadContacts(Long customerId) { private List<CustomerContactVO> loadContacts(Long customerId) {
@@ -463,10 +463,10 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
// 同步口径:按业务日期窗口拉取金蝶收款单(收款处理),以金蝶明细流水号为幂等键 upsert 收款流水表; // 同步口径:按业务日期窗口拉取金蝶收款单(收款处理),以金蝶明细流水号为幂等键 upsert 收款流水表;
// 本地已认领金额/状态(claimedAmount/claimStatus)由认领、作废驱动,不被同步顺带覆盖, // 本地已认领金额/状态(claimedAmount/claimStatus)由认领、作废驱动,不被同步顺带覆盖,
// 未匹配收款金额单独存 unmatch_amount_rec,仅作金蝶参考展示,不参与认领额度。 // 未匹配收款金额单独存 unmatch_amount_rec,仅作金蝶参考展示,不参与认领额度。
// 按组织过滤:业务部门配置中已维护的金蝶公司代码 // 按组织过滤:组织管理(blade_dept)中平台公司(是否平台公司=是)且已维护金蝶公司代码的组织
List<String> orgNumbers = baseMapper.selectPlatformOrgNumbers(); List<String> orgNumbers = baseMapper.selectPlatformOrgNumbers();
if (Func.isEmpty(orgNumbers)) { if (Func.isEmpty(orgNumbers)) {
throw new ServiceException("业务部门配置中未维护金蝶公司代码,无法同步金蝶收款流水,请先检查业务部门配置"); throw new ServiceException("组织管理中未维护平台公司金蝶公司代码,无法同步金蝶收款流水,请先检查组织数据");
} }
log.info("收款流水同步按组织过滤,组织编码:{}", orgNumbers); log.info("收款流水同步按组织过滤,组织编码:{}", orgNumbers);
RecBillSyncState state = new RecBillSyncState(); RecBillSyncState state = new RecBillSyncState();
@@ -586,9 +586,9 @@ public class KingdeeApBillSyncSupport {
String feeType = Func.toStr(summaryFee.getFeeType()).trim(); String feeType = Func.toStr(summaryFee.getFeeType()).trim();
String feeItem = Func.toStr(summaryFee.getFeeItem()).trim(); String feeItem = Func.toStr(summaryFee.getFeeItem()).trim();
String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]"; String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]";
// 物料:运费类按项目业务模式/运输方式映射,仓储大类(档案编码 A05)映射仓储物料,其余非运费类映射装卸物料 // 物料:运费类按项目业务模式/运输方式映射,仓储大类(档案编码/费用类型 A05)映射仓储物料,其余非运费类映射装卸物料
FeeCategory feeCategory = isFreightCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.FREIGHT FeeCategory feeCategory = isFreightCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.FREIGHT
: isWarehouseCategory(feeItem, context.feeCategoryByName()) ? FeeCategory.WAREHOUSE : FeeCategory.LOADING; : isWarehouseCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.WAREHOUSE : FeeCategory.LOADING;
String materialNumber = resolveMaterialNumber(feeCategory, context.project(), fallbackTransportType, sourceDesc); String materialNumber = resolveMaterialNumber(feeCategory, context.project(), fallbackTransportType, sourceDesc);
// 结算合计费用项行无运输量,数量默认 1 // 结算合计费用项行无运输量,数量默认 1
BigDecimal quantity = BigDecimal.ONE; BigDecimal quantity = BigDecimal.ONE;
@@ -798,12 +798,15 @@ public class KingdeeApBillSyncSupport {
} }
/** /**
* 仓储大类判定(手动添加结算合计费用行物料映射用):费用项档案维护的费用大类编码为 A05 归仓储大类, * 仓储大类判定(手动添加结算合计费用行物料映射用):费用项档案维护的费用大类编码为 A05 归仓储大类;
* 无名称关键字兜底,分类以档案维护为准 * 档案未维护该费用项时回退行费用类型编码 A05;无名称关键字兜底,分类以档案/费用类型维护为准
*/ */
static boolean isWarehouseCategory(String feeItemName, Map<String, String> feeCategoryByName) { public static boolean isWarehouseCategory(String feeTypeCode, String feeItemName, Map<String, String> feeCategoryByName) {
String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName); String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName);
return FEE_CATEGORY_WAREHOUSE.equals(categoryCode); if (Func.isNotBlank(categoryCode)) {
return FEE_CATEGORY_WAREHOUSE.equals(categoryCode);
}
return FEE_CATEGORY_WAREHOUSE.equals(feeTypeCode);
} }
/** 装卸大类费用项判定(关键字兜底):费用项名称含“装卸” */ /** 装卸大类费用项判定(关键字兜底):费用项名称含“装卸” */
@@ -1055,7 +1058,7 @@ public class KingdeeApBillSyncSupport {
} }
/** /**
* 合同金蝶组织编码:取业务部门配置中的金蝶公司代码(平台公司 + 业务部门) * 合同金蝶组织编码:合同关联项目 → 项目业务部门沿部门树向上反查唯一平台公司 → 平台公司金蝶公司代码
* *
* @param contract 合同实体 * @param contract 合同实体
* @param context 金蝶同步上下文(缓存载体) * @param context 金蝶同步上下文(缓存载体)
@@ -1065,14 +1068,28 @@ public class KingdeeApBillSyncSupport {
if (contract == null) { if (contract == null) {
throw new ServiceException("推送金蝶失败,合同不存在,无法确定金蝶组织编码"); throw new ServiceException("推送金蝶失败,合同不存在,无法确定金蝶组织编码");
} }
return this.kingdeeCompanyNumber(contract, contract.getProjectId(), context); // 优先取合同平台公司金蝶代码;未维护时回退项目业务部门上溯
if (contract.getPlatformCompanyId() != null) {
Dept platformCompany = SysCache.getDept(contract.getPlatformCompanyId());
if (platformCompany == null) {
throw new ServiceException("推送金蝶失败,合同平台公司不存在,无法确定金蝶组织编码");
}
if (Func.isBlank(platformCompany.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,平台公司[" + platformCompany.getDeptName() + "]未维护金蝶公司代码,请检查组织数据");
}
return platformCompany.getKingdeeDeptCode();
}
if (contract.getProjectId() == null) {
throw new ServiceException("推送金蝶失败,合同[" + Func.toStr(contract.getContractNo()) + "]未关联项目,无法确定金蝶组织编码");
}
return this.kingdeeOrgNumber(contract.getProjectId(), context);
} }
/** /**
* 金蝶组织编码:指定部门沿部门树向上反查平台公司,再按「平台公司 + 业务部门」取业务部门配置中的金蝶公司代码。 * 金蝶组织编码:指定部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶公司代码。
* 先票后款链路收票登记不挂合同/项目,结算组织由页面手选部门派生(A303-2 org_number) * 先票后款链路收票登记不挂合同/项目,结算组织由页面手选部门派生(A303-2 org_number)
* *
* @param deptId 手选部门id(收票登记 deptId,须为业务部门) * @param deptId 手选部门id(收票登记 deptId)
* @param context 金蝶同步上下文(缓存载体) * @param context 金蝶同步上下文(缓存载体)
* @return 组织编码 * @return 组织编码
*/ */
@@ -1084,78 +1101,38 @@ public class KingdeeApBillSyncSupport {
if (dept == null) { if (dept == null) {
throw new ServiceException("推送金蝶失败,所选结算组织[" + deptId + "]不存在,无法确定金蝶组织编码"); throw new ServiceException("推送金蝶失败,所选结算组织[" + deptId + "]不存在,无法确定金蝶组织编码");
} }
if (!Objects.equals(dept.getIsBusinessDept(), 1)) { return this.platformCompanyOrgNumber(dept, dept.getDeptName());
throw new ServiceException("推送金蝶失败,所选结算组织[" + dept.getDeptName() + "]不是业务部门,无法确定金蝶组织编码");
}
Long platformCompanyId = this.resolvePlatformCompanyId(dept);
if (platformCompanyId == null) {
throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(dept.getDeptName()) + "]未归属平台公司,无法确定金蝶组织编码");
}
return this.requireKingdeeCompanyCode(platformCompanyId, deptId);
} }
/** /**
* 金蝶组织编码:按项目业务部门配置中的金蝶公司代码推送 * 金蝶组织编码:项目业务部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶公司代码;
* 项目未关联/未维护业务部门/业务部门未归属平台公司/平台公司未维护金蝶公司代码时拦截
* *
* @param projectId 项目id * @param projectId 项目id
* @param context 金蝶同步上下文(缓存载体) * @param context 金蝶同步上下文(缓存载体)
* @return 组织编码 * @return 组织编码
*/ */
public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) { public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) {
return this.kingdeeCompanyNumber(null, projectId, context); ProjectApply project = this.requireOrgProject(projectId, context);
Dept dept = SysCache.getDept(project.getBusinessDeptId());
if (dept == null) {
throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]业务部门不存在,无法确定金蝶组织编码");
}
return this.platformCompanyOrgNumber(dept, project.getBusinessDeptName());
} }
/** /** 部门树向上反查平台公司金蝶公司代码(上限 20 级防脏数据成环死循环),未归属平台公司时拦截 */
* 金蝶公司代码:按合同平台公司下业务部门配置推送 private String platformCompanyOrgNumber(Dept dept, String startDeptName) {
*
* @param contract 合同(优先取其平台公司、合同负责部门)
* @param projectId 项目id(合同未带业务部门时回退项目业务部门)
* @param context 金蝶同步上下文(缓存载体)
* @return 公司编码
*/
private String kingdeeCompanyNumber(ContractManage contract, Long projectId, KingdeeSyncContext context) {
Long platformCompanyId = contract == null ? null : contract.getPlatformCompanyId();
Long businessDeptId = contract == null ? null : contract.getOrganizationId();
if (businessDeptId == null && projectId != null) {
ProjectApply project = this.requireOrgProject(projectId, context);
businessDeptId = project.getBusinessDeptId();
}
if (platformCompanyId == null && businessDeptId != null) {
Dept businessDept = SysCache.getDept(businessDeptId);
if (businessDept == null) {
throw new ServiceException("推送金蝶失败,业务部门不存在,无法确定金蝶组织编码");
}
platformCompanyId = this.resolvePlatformCompanyId(businessDept);
if (platformCompanyId == null) {
throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(businessDept.getDeptName())
+ "]未归属平台公司,无法确定金蝶组织编码");
}
}
if (platformCompanyId == null || businessDeptId == null) {
throw new ServiceException("业务部门对应的金蝶公司代码未配置,单据无法推送,请联系管理员处理。");
}
return this.requireKingdeeCompanyCode(platformCompanyId, businessDeptId);
}
/** 读取业务部门配置中的金蝶公司代码,未配置时拦截 */
private String requireKingdeeCompanyCode(Long platformCompanyId, Long businessDeptId) {
org.springblade.system.pojo.entity.BusinessDeptConfig config =
SysCache.getBusinessDeptConfig(platformCompanyId, businessDeptId);
if (config == null || Func.isBlank(config.getKingdeeCompanyCode())) {
throw new ServiceException("业务部门对应的金蝶公司代码未配置,单据无法推送,请联系管理员处理。");
}
return config.getKingdeeCompanyCode().trim();
}
/** 部门树向上反查平台公司ID(上限 20 级防脏数据成环死循环) */
private Long resolvePlatformCompanyId(Dept dept) {
for (int depth = 0; dept != null && depth < 20; depth++) { for (int depth = 0; dept != null && depth < 20; depth++) {
if (Objects.equals(dept.getIsPlatformCompany(), 1)) { if (Objects.equals(dept.getIsPlatformCompany(), 1)) {
return dept.getId(); if (Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,平台公司[" + dept.getDeptName() + "]未维护金蝶公司代码,请检查组织数据");
}
return dept.getKingdeeDeptCode();
} }
dept = dept.getParentId() == null ? null : SysCache.getDept(dept.getParentId()); dept = dept.getParentId() == null ? null : SysCache.getDept(dept.getParentId());
} }
return null; throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(startDeptName) + "]未归属平台公司,无法确定金蝶组织编码");
} }
/** /**
@@ -185,4 +185,115 @@ class KingdeeEntrySplitTest {
assertFalse(KingdeeApBillSyncSupport.isFreightCategory("A10", "人工装卸费用", Map.of())); assertFalse(KingdeeApBillSyncSupport.isFreightCategory("A10", "人工装卸费用", Map.of()));
assertTrue(KingdeeApBillSyncSupport.isFreightCategory("", "运输费", Map.of())); assertTrue(KingdeeApBillSyncSupport.isFreightCategory("", "运输费", Map.of()));
} }
/** 仓储同税率合并:同货物行档案 A05 的仓储费 + 堆存费同税率合并一行 */
@Test
void mergeWarehouseSameTaxRate() {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
feeItems.put("仓储费", new BigDecimal("300"));
feeItems.put("堆存费", new BigDecimal("200"));
Map<String, String> feeCategoryByName = Map.of(
"仓储费", "A05",
"堆存费", "A05");
Map<String, BigDecimal> taxRateByRule = Map.of(
"仓储费", new BigDecimal("6"),
"堆存费", new BigDecimal("6"));
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName, taxRateByRule,
new BigDecimal("500"));
assertEquals(1, splits.size());
assertTrue(splits.get(0).isWarehouse());
assertEquals(new BigDecimal("6"), splits.get(0).getTaxRate());
assertEquals(new BigDecimal("500"), splits.get(0).getAmount());
assertEquals("-6", splits.get(0).taxRateSuffix());
}
/** 仓储不同税率拆行:同大类不同税率各生成一行 */
@Test
void splitWarehouseByTaxRate() {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
feeItems.put("仓储费", new BigDecimal("300"));
feeItems.put("保管费", new BigDecimal("200"));
Map<String, String> feeCategoryByName = Map.of(
"仓储费", "A05",
"保管费", "A05");
Map<String, BigDecimal> taxRateByRule = Map.of(
"仓储费", new BigDecimal("6"),
"保管费", new BigDecimal("9"));
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName, taxRateByRule,
new BigDecimal("500"));
assertEquals(2, splits.size());
assertTrue(splits.get(0).isWarehouse());
assertEquals(new BigDecimal("6"), splits.get(0).getTaxRate());
assertEquals(new BigDecimal("300"), splits.get(0).getAmount());
assertEquals("-6", splits.get(0).taxRateSuffix());
assertTrue(splits.get(1).isWarehouse());
assertEquals(new BigDecimal("9"), splits.get(1).getTaxRate());
assertEquals(new BigDecimal("200"), splits.get(1).getAmount());
assertEquals("-9", splits.get(1).taxRateSuffix());
}
/** 仓储不混入运输组:运输费与档案 A05 仓储费按大类拆开 */
@Test
void warehouseNotMergedIntoFreight() {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
feeItems.put("运输费", new BigDecimal("1000"));
feeItems.put("仓储费", new BigDecimal("500"));
Map<String, String> feeCategoryByName = Map.of("仓储费", "A05");
Map<String, BigDecimal> taxRateByRule = Map.of(
"运输费", new BigDecimal("9"),
"仓储费", new BigDecimal("6"));
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName, taxRateByRule,
new BigDecimal("1500"));
assertEquals(2, splits.size());
assertFalse(splits.get(0).isWarehouse());
assertFalse(splits.get(0).isLoading());
assertEquals(new BigDecimal("1000"), splits.get(0).getAmount());
assertTrue(splits.get(1).isWarehouse());
assertEquals(new BigDecimal("500"), splits.get(1).getAmount());
}
/** 档案未配 A05 不做名称关键字兜底:「仓储服务费」档案 A02 仍归运输组 */
@Test
void warehouseWithoutArchiveCodeStaysFreight() {
Map<String, BigDecimal> feeItems = Map.of("仓储服务费", new BigDecimal("500"));
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems,
Map.of("仓储服务费", "A02"), Map.of(), new BigDecimal("500"));
assertEquals(1, splits.size());
assertFalse(splits.get(0).isWarehouse());
assertFalse(splits.get(0).isLoading());
assertEquals(new BigDecimal("500"), splits.get(0).getAmount());
}
/** 残差勾稽不变:含仓储费时残差仍并入运输无税率组 */
@Test
void residualStillGoesToFreightWhenWarehousePresent() {
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
feeItems.put("运输费", new BigDecimal("2000"));
feeItems.put("仓储费", new BigDecimal("500"));
Map<String, String> feeCategoryByName = Map.of("仓储费", "A05");
List<FeeLineSplit> splits = KingdeeApBillSyncSupport.splitFeeItems(feeItems, feeCategoryByName,
Map.of("运输费", new BigDecimal("9")), new BigDecimal("2520"));
assertEquals(3, splits.size());
assertEquals(new BigDecimal("2000"), splits.get(0).getAmount());
assertTrue(splits.get(1).isWarehouse());
assertEquals(new BigDecimal("500"), splits.get(1).getAmount());
assertFalse(splits.get(2).isWarehouse());
assertFalse(splits.get(2).isLoading());
assertNull(splits.get(2).getTaxRate());
assertEquals(new BigDecimal("20"), splits.get(2).getAmount());
}
/** 手工合计行仓储判定:档案 A05 或费用类型编码 A05 归仓储物料 */
@Test
void warehouseCategoryMatchesArchiveOrFeeType() {
Map<String, String> feeCategoryByName = Map.of("仓储堆存保管费", "A05");
assertTrue(KingdeeApBillSyncSupport.isWarehouseCategory("A05", "仓储堆存保管费", feeCategoryByName));
assertFalse(KingdeeApBillSyncSupport.isWarehouseCategory("A05", "人工装卸费用",
Map.of("人工装卸费用", "A10")));
// 档案未维护:回退行费用类型编码 A05
assertTrue(KingdeeApBillSyncSupport.isWarehouseCategory("A05", "临时仓储杂费", Map.of()));
assertFalse(KingdeeApBillSyncSupport.isWarehouseCategory("A10", "临时仓储杂费", Map.of()));
// 无名称关键字兜底:名称含仓储但档案/费用类型均非 A05 不归仓储
assertFalse(KingdeeApBillSyncSupport.isWarehouseCategory("A02", "仓储服务费", Map.of()));
}
} }
+1 -1
View File
@@ -303,7 +303,7 @@ CREATE TABLE `blade_dept` (
`dept_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名', `dept_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名',
`full_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门全称', `full_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门全称',
`dept_code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织编码', `dept_code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织编码',
`kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶部门代码', `kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶公司代码',
`contract_code_prefix` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '合同编码前缀', `contract_code_prefix` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '合同编码前缀',
`contract_initial_serial` int NULL DEFAULT NULL COMMENT '合同初始流水号', `contract_initial_serial` int NULL DEFAULT NULL COMMENT '合同初始流水号',
`short_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织简称', `short_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织简称',
@@ -0,0 +1,11 @@
-- 金蝶公司代码改回组织(平台公司)维护:业务部门配置仅作展示镜像
-- 1)用平台公司组织上的金蝶编码回填业务部门配置展示字段
UPDATE `blade_business_dept_config` c
INNER JOIN `blade_dept` d ON d.id = c.platform_company_id AND d.is_deleted = 0
SET c.kingdee_company_code = d.kingdee_dept_code
WHERE d.kingdee_dept_code IS NOT NULL
AND d.kingdee_dept_code != '';
-- 2)字段注释改为组织回填展示口径
ALTER TABLE `blade_business_dept_config`
MODIFY COLUMN `kingdee_company_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶公司代码(来自平台公司组织,仅展示)';
@@ -0,0 +1,44 @@
-- 客商评分双模式与下次评定日期:评分记录增加评估方式/下次评定日期,档案主表增加下次评定日期供列表展示
-- 幂等可重复执行;排序规则 utf8mb4_general_ci
-- ① 评分记录:评估方式 online/offline
SET @score_mode_col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'blade_customer_credit_score'
AND column_name = 'evaluate_mode'
);
SET @score_mode_sql := IF(@score_mode_col = 0,
'ALTER TABLE `blade_customer_credit_score` ADD COLUMN `evaluate_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''online'' COMMENT ''评估方式:online线上评估,offline线下评估'' AFTER `quantification_id`',
'SELECT 1');
PREPARE score_mode_stmt FROM @score_mode_sql;
EXECUTE score_mode_stmt;
DEALLOCATE PREPARE score_mode_stmt;
-- ② 评分记录:下次评定日期
SET @score_next_col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'blade_customer_credit_score'
AND column_name = 'next_evaluate_date'
);
SET @score_next_sql := IF(@score_next_col = 0,
'ALTER TABLE `blade_customer_credit_score` ADD COLUMN `next_evaluate_date` date NULL DEFAULT NULL COMMENT ''下次评定日期'' AFTER `score_date`',
'SELECT 1');
PREPARE score_next_stmt FROM @score_next_sql;
EXECUTE score_next_stmt;
DEALLOCATE PREPARE score_next_stmt;
-- ③ 客商档案:下次评定日期(审批通过后由评分写回)
SET @archive_next_col := (
SELECT COUNT(1) FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'blade_customer_archive'
AND column_name = 'next_evaluate_date'
);
SET @archive_next_sql := IF(@archive_next_col = 0,
'ALTER TABLE `blade_customer_archive` ADD COLUMN `next_evaluate_date` date NULL DEFAULT NULL COMMENT ''下次评定日期'' AFTER `business_end_date`',
'SELECT 1');
PREPARE archive_next_stmt FROM @archive_next_sql;
EXECUTE archive_next_stmt;
DEALLOCATE PREPARE archive_next_stmt;
@@ -0,0 +1,3 @@
-- 组织表:平台公司金蝶编码字段注释由「金蝶部门代码」改为「金蝶公司代码」(字段名不变)
ALTER TABLE `blade_dept`
MODIFY COLUMN `kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶公司代码';
@@ -0,0 +1,25 @@
-- 金蝶分录仓储大类(A102-2/A302-2/A103-1 分录拆分按费用项档案分类编码判定仓储大类,无名称关键字兜底)。
-- ① 业务字典 fee_category 新增子项 A05 仓储;② 费用项档案仓储类费用项归入 A05(分类生效前提)。
-- id 由 UUID_SHORT() 动态生成,幂等可重复执行。
-- 排序规则:utf8mb4_general_ci(与业务表一致)
-- ① 业务字典 fee_category 新增子项:A05 仓储
INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
SELECT UUID_SHORT(), '000000',
(SELECT id FROM blade_dict_biz WHERE code = 'fee_category' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
'fee_category', 'A05', '仓储', 1, '金蝶分录仓储大类', 0, 1, 0
WHERE NOT EXISTS (
SELECT 1 FROM blade_dict_biz
WHERE code = 'fee_category' AND dict_key = 'A05' AND tenant_id = '000000' AND is_deleted = 0
);
-- ② 费用项档案:仓储类费用项归入 A05(已执行则更新 0 行,幂等)
-- 执行前请先核对命中清单,避免把名称含「保管」但非仓储的费用项误归仓:
-- SELECT id, name, fee_category FROM blade_fee_item
-- WHERE is_deleted = 0 AND fee_category <> 'A05'
-- AND (name LIKE '%仓储%' OR name LIKE '%堆存%' OR name LIKE '%保管%');
UPDATE blade_fee_item
SET fee_category = 'A05'
WHERE is_deleted = 0
AND fee_category <> 'A05'
AND (name LIKE '%仓储%' OR name LIKE '%堆存%' OR name LIKE '%保管%');