✨ 新增业务部门配置并按平台公司生成合同号与金蝶部门码

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 21:10:45 +08:00
parent 91dddd0e02
commit fd3ca7496d
24 changed files with 912 additions and 40 deletions
@@ -188,10 +188,11 @@ public class ContractManageController extends BladeController {
@GetMapping("/next-contract-no")
@ApiOperationSupport(order = 16)
@Operation(summary = "预览下一合同编号", description = "按合同负责部门编码前缀+年份+三位流水预览")
@Operation(summary = "预览下一合同编号", description = "按平台公司+业务部门配置的前缀+年份+三位流水预览")
public R<String> nextContractNo(
@Parameter(description = "合同负责部门ID", required = true) @RequestParam Long organizationId) {
return R.data(contractManageService.nextContractNo(organizationId));
@Parameter(description = "合同负责部门ID", required = true) @RequestParam Long organizationId,
@Parameter(description = "平台公司ID", required = true) @RequestParam Long platformCompanyId) {
return R.data(contractManageService.nextContractNo(organizationId, platformCompanyId));
}
@GetMapping("/export-contract-manage")
@@ -75,12 +75,13 @@ public interface IContractManageService extends BaseService<ContractManage> {
List<ContractManageExcel> exportContractManage(ContractManageVO contractManage, String ids);
/**
* 预览下一合同编号(按合同负责部门前缀 + 年份 + 三位流水)
* 预览下一合同编号(按平台公司 + 业务部门配置的前缀 + 年份 + 三位流水)
*
* @param organizationId 合同负责部门ID
* @param organizationId 合同负责部门(业务部门)ID
* @param platformCompanyId 平台公司ID
* @return 预览编号
*/
String nextContractNo(Long organizationId);
String nextContractNo(Long organizationId, Long platformCompanyId);
/**
* 合同是否可用于生成结算单:已审批、未终止,且临时合同仍在临时效力期内。
@@ -36,6 +36,7 @@ import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.BusinessDeptConfig;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.ContractManageExcel;
@@ -736,6 +737,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
contractManage.setOrganizationId(oldRecord.getOrganizationId());
contractManage.setOrganizationName(oldRecord.getOrganizationName());
}
if (Func.isEmpty(contractManage.getPlatformCompanyId())) {
contractManage.setPlatformCompanyId(oldRecord.getPlatformCompanyId());
contractManage.setPlatformCompanyName(oldRecord.getPlatformCompanyName());
}
contractManage.setHandlerUserId(oldRecord.getHandlerUserId());
contractManage.setHandlerUserName(oldRecord.getHandlerUserName());
// 归档状态仅由审核通过逻辑自动维护,保存时保留库中已有值。
@@ -760,7 +765,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
if (!generateWhenEmpty) {
throw new ServiceException("请输入合同编号");
}
normalizedContractNo = nextContractNo(contractManage.getOrganizationId());
normalizedContractNo = nextContractNo(contractManage.getOrganizationId(), contractManage.getPlatformCompanyId());
}
TransportBusinessSupport.validateLength(normalizedContractNo, 100, "合同编号不能超过100字");
String currentContractNo = TransportBusinessSupport.trimToNull(contractManage.getContractNo());
@@ -819,30 +824,39 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
}
@Override
public synchronized String nextContractNo(Long organizationId) {
public synchronized String nextContractNo(Long organizationId, Long platformCompanyId) {
if (Func.isEmpty(organizationId)) {
throw new ServiceException("请选择合同负责部门");
}
if (Func.isEmpty(platformCompanyId)) {
throw new ServiceException("请选择平台公司");
}
Dept dept = SysCache.getDept(organizationId);
if (dept == null) {
throw new ServiceException("合同负责部门不存在");
}
String prefix = TransportBusinessSupport.trimToNull(dept.getContractCodePrefix());
Dept platformCompany = SysCache.getDept(platformCompanyId);
if (platformCompany == null) {
throw new ServiceException("平台公司不存在");
}
BusinessDeptConfig config = SysCache.getBusinessDeptConfig(platformCompanyId, organizationId);
String prefix = config == null ? null : TransportBusinessSupport.trimToNull(config.getContractCodePrefix());
if (Func.isEmpty(prefix)) {
throw new ServiceException("请先在组织机构中配置该部门的合同编码前缀");
throw new ServiceException("部门合同前缀未配置,无法自动生成,请手动输入合同编号");
}
int year = LocalDate.now().getYear();
String head = prefix + "(" + year + ")";
// 按前缀+年份扫描已占用流水号;系统已有相同流水号则跳过
List<ContractManage> latestList = list(Wrappers.<ContractManage>lambdaQuery()
.select(ContractManage::getContractNo)
.eq(ContractManage::getIsDeleted, 0)
.eq(ContractManage::getOrganizationId, organizationId)
.likeRight(ContractManage::getContractNo, head)
.orderByDesc(ContractManage::getContractNo)
.last("LIMIT 50"));
.last("LIMIT 200"));
Pattern pattern = Pattern.compile("^" + Pattern.quote(prefix) + "(" + year + ")(\\d{3})号$");
// 当年尚无匹配编号时,从组织配置的初始流水号起编;否则在已有最大流水号上加1
int next = resolveContractInitialSerial(dept);
int initialSerial = resolveContractInitialSerial(config);
int maxUsed = 0;
boolean hasMatched = false;
for (ContractManage item : latestList) {
String contractNo = TransportBusinessSupport.trimToNull(item.getContractNo());
if (Func.isEmpty(contractNo)) {
@@ -852,22 +866,24 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
if (!matcher.matches()) {
continue;
}
next = Integer.parseInt(matcher.group(1)) + 1;
break;
hasMatched = true;
maxUsed = Math.max(maxUsed, Integer.parseInt(matcher.group(1)));
}
// 从初始流水号起编;若系统已有相同或更大流水号则跳到 maxUsed+1
int next = hasMatched ? Math.max(initialSerial, maxUsed + 1) : initialSerial;
if (next > 999) {
throw new ServiceException("当前部门本年合同流水号已达上限999");
}
return head + String.format("%03d", next) + "号";
}
private int resolveContractInitialSerial(Dept dept) {
Integer serial = dept.getContractInitialSerial();
private int resolveContractInitialSerial(BusinessDeptConfig config) {
Integer serial = config == null ? null : config.getInitialSerial();
if (serial == null || serial < 1) {
return 1;
}
if (serial > 999) {
throw new ServiceException("组织机构中配置的合同初始流水号不能超过999");
throw new ServiceException("业务部门配置的合同初始流水号不能超过999");
}
return serial;
}
@@ -1163,6 +1179,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
TransportBusinessSupport.validateRequired(contractManage.getContractCategory(), "请选择合同类型");
TransportBusinessSupport.validateRequired(contractManage.getProjectName(), "请选择所属项目");
TransportBusinessSupport.validateRequired(contractManage.getOrganizationName(), "请选择合同负责部门");
TransportBusinessSupport.validateRequired(contractManage.getPlatformCompanyName(), "请选择平台公司");
ContractPartySupport.validateRequiredParties(contractManage, true);
ContractPartySupport.validateBillingPlanBindings(contractManage);
validateContractAmountRequired(contractManage);
@@ -1197,6 +1214,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
TransportBusinessSupport.validateRequired(contractManage.getContractCategory(), "请选择合同类型");
TransportBusinessSupport.validateRequired(contractManage.getProjectName(), "请选择所属项目");
TransportBusinessSupport.validateRequired(contractManage.getOrganizationName(), "请选择合同负责部门");
TransportBusinessSupport.validateRequired(contractManage.getPlatformCompanyName(), "请选择平台公司");
TransportBusinessSupport.validateRequired(contractManage.getSettlementCurrency(), "请选择结算币种");
TransportBusinessSupport.validateRequired(contractManage.getPartyA(), "请输入甲方");
TransportBusinessSupport.validateRequired(contractManage.getPartyB(), "请输入乙方");
@@ -818,10 +818,16 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
}
if (Func.isBlank(entity.getDepartmentEmails())) throw new ServiceException("推送金蝶开票申请失败:部门邮箱(发票推送邮箱)不能为空");
if (Func.isBlank(entity.getTaxpayerNo())) throw new ServiceException("推送金蝶开票申请失败:购方纳税人识别号不能为空");
// 金蝶组织编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码;申请部门编码:项目业务部门自身金蝶编码(与付款申请单部门.编码同源,共用上下文缓存)
// 金蝶组织/申请部门:按首张关联结算单合同的平台公司 + 业务部门配置取数
KingdeeSyncContext kingdeeContext = KingdeeSyncContext.empty();
String kingdeeOrgNumber = kingdeeApBillSyncSupport.kingdeeOrgNumber(entity.getProjectId(), kingdeeContext);
String kingdeeDeptNumber = kingdeeApBillSyncSupport.kingdeeDeptNumber(entity.getProjectId(), kingdeeContext);
FormalSettlement firstSettlement = formalSettlementMapper.selectById(relations.get(0).getFormalSettlementId());
if (firstSettlement == null || Objects.equals(firstSettlement.getIsDeleted(), 1)) {
throw new ServiceException("推送金蝶开票申请失败:正式结算单["
+ Func.toStr(relations.get(0).getFormalSettlementNo()) + "]不存在");
}
ContractManage contract = kingdeeApBillSyncSupport.requireContract(firstSettlement.getContractId(), kingdeeContext);
String kingdeeOrgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, kingdeeContext);
String kingdeeDeptNumber = kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, entity.getProjectId(), kingdeeContext);
CustomerArchive customer = entity.getReceiverCustomerId() == null ? null : customerArchiveMapper.selectById(entity.getReceiverCustomerId());
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护客商编码,请补充后重试");
@@ -909,9 +915,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
String firstArSrcCode, BigDecimal sheetAmount, List<BfiInvoiceApplySaveEntryDTO> items) {
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
// 【必填】组织.编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
// 【必填】组织.编码:合同平台公司金蝶组织编码
param.setOrgidNumber(orgNumber);
// 【必填】申请部门.编码:项目业务部门金蝶编码(与付款申请单 awrp_department_number 同源)
// 【必填】申请部门.编码:合同平台公司下业务部门配置的金蝶部门代码
param.setAwrpOrgfieldNumber(deptNumber);
// 【必填】业务单据编号(候选键):单张发票候选 billno(多张发票时开票申请单号-张次序号)
param.setBillno(billNo);
@@ -1254,8 +1254,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
param.setAwrpSrcSys(bfiProperties.getSrcSys());
// 结算组织.编码
param.setSettleorgNumber(orgNumber);
// 部门.编码:项目业务部门自身的金蝶编码(结算/申请/付款组织仍传上溯平台公司编码)
param.setAwrpDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract.getProjectId(), context));
// 部门.编码:合同平台公司下业务部门配置的金蝶部门代码
param.setAwrpDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, contract.getProjectId(), context));
// 【必填】单据类型.编码
param.setBilltypeNumber(bfiProperties.getPayApply().getBilltype());
// 【必填】申请日期:yyyy-MM-dd HH:mm:ss(各接口统一格式)
@@ -282,8 +282,8 @@ public class KingdeeApBillSyncSupport {
param.setCurrencyNumber(currency);
// 【必填】付款组织.编码
param.setPayorgNumber(orgNumber);
// 部门.编码:项目业务部门自身的金蝶编码(结算/付款组织仍传上溯平台公司编码,口径与付款申请 A103-3 一致)
param.setDepartmentNumber(this.kingdeeDeptNumber(contract.getProjectId(), context));
// 部门.编码:合同平台公司下业务部门配置的金蝶部门代码
param.setDepartmentNumber(this.kingdeeDeptNumber(contract, contract.getProjectId(), context));
// 采购组织.编码(非必传)
// param.setPurorgNumber(orgNumber);
// 【必填】款项性质.编码
@@ -962,6 +962,20 @@ public class KingdeeApBillSyncSupport {
* @return 组织编码
*/
public String bfiOrgNumber(ContractManage contract, KingdeeSyncContext context) {
if (contract == null) {
throw new ServiceException("推送金蝶失败,合同不存在,无法确定金蝶组织编码");
}
// 优先取合同平台公司金蝶代码;未维护时回退项目业务部门上溯
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()) + "]未关联项目,无法确定金蝶组织编码");
}
@@ -993,20 +1007,41 @@ public class KingdeeApBillSyncSupport {
}
/**
* 金蝶部门编码:项目业务部门自身的金蝶编码(blade_dept.kingdee_dept_code,不上溯平台公司),
* 用于付款申请单 awrp_department_number(部门.编码);项目未关联/未维护业务部门/业务部门未维护金蝶编码时拦截
* 金蝶部门编码:必须取「业务部门配置」中合同平台公司 + 业务部门的金蝶部门代码;
* 未配置时拦截推送
*
* @param projectId 项目id
* @param context 金蝶同步上下文(缓存载体)
* @return 部门编码
*/
public String kingdeeDeptNumber(Long projectId, KingdeeSyncContext context) {
ProjectApply project = this.requireOrgProject(projectId, context);
Dept dept = SysCache.getDept(project.getBusinessDeptId());
if (dept == null || Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未维护金蝶编码,请检查部门数据");
return kingdeeDeptNumber(null, projectId, context);
}
/**
* 金蝶部门编码:按合同平台公司下业务部门配置的金蝶部门代码推送
*
* @param contract 合同(优先取其平台公司、合同负责部门)
* @param projectId 项目id(合同未带业务部门时回退项目业务部门)
* @param context 金蝶同步上下文(缓存载体)
* @return 部门编码
*/
public String kingdeeDeptNumber(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();
}
return dept.getKingdeeDeptCode();
if (platformCompanyId == null || businessDeptId == null) {
throw new ServiceException("业务部门对应的金蝶组织代码未配置,单据无法推送,请联系管理员处理。");
}
org.springblade.system.pojo.entity.BusinessDeptConfig config =
SysCache.getBusinessDeptConfig(platformCompanyId, businessDeptId);
if (config == null || Func.isBlank(config.getKingdeeDeptCode())) {
throw new ServiceException("业务部门对应的金蝶组织代码未配置,单据无法推送,请联系管理员处理。");
}
return config.getKingdeeDeptCode().trim();
}
/** 金蝶组织/部门编码共用校验:项目存在且已维护业务部门 */
@@ -218,8 +218,8 @@ public class KingdeeArBillSyncSupport {
param.setOrgNumber(orgNumber);
// 收款组织.编码:同结算组织
param.setRecorgNumber(orgNumber);
// 部门.编码:项目业务部门自身的金蝶编码(结算/收款组织仍传上溯平台公司编码,口径与付款申请/应付单一致)
param.setDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract.getProjectId(), context));
// 部门.编码:合同平台公司下业务部门配置的金蝶部门代码
param.setDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, contract.getProjectId(), context));
// 【必填】单据日期
param.setBizdate(billDateText);
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
@@ -185,7 +185,9 @@ public class KingdeeReceivedInvoiceSyncSupport {
param.setPaymenttype(BfiConstant.PAY_MODE_CASH);
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
// 【必填】结算组织.编码:合同关联项目业务部门反查平台公司(与应付单同源)
// 部门金蝶代码校验:收票单主表无部门字段,推送前仍按合同平台公司下业务部门配置校验
kingdeeApBillSyncSupport.kingdeeDeptNumber(contract, contract.getProjectId(), context);
// 【必填】结算组织.编码:合同平台公司金蝶组织编码(与应付单同源)
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context));
// 【必填】结算币别:合同结算币种
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));