摘要生成收付款单-第一次提交

This commit is contained in:
李贵强 2025-10-30 11:10:44 +08:00
parent c76fd12984
commit 2ab4bee1d3
3 changed files with 553 additions and 9 deletions

View File

@ -0,0 +1,369 @@
package shjh.jhzj7.fi.fi.plugin.convert;
import kd.bos.dataentity.entity.DynamicObject;
import kd.bos.dataentity.entity.DynamicObjectCollection;
import kd.bos.entity.ExtendedDataEntity;
import kd.bos.entity.ExtendedDataEntitySet;
import kd.bos.entity.botp.plugin.AbstractConvertPlugIn;
import kd.bos.entity.botp.plugin.args.AfterConvertEventArgs;
import kd.bos.logging.Log;
import kd.bos.logging.LogFactory;
import kd.bos.orm.query.QCP;
import kd.bos.orm.query.QFilter;
import kd.bos.servicehelper.BusinessDataServiceHelper;
import kd.sdk.plugin.Plugin;
import kd.tmc.fbp.common.helper.TmcBotpHelper;
import kd.tmc.fbp.common.helper.TmcDataServiceHelper;
import kd.tmc.fbp.common.util.EmptyUtil;
import shjh.jhzj7.fi.fi.utils.StrUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
* 智能付款单转换插件
* 功能根据摘要解析规则生成付款单分录
*/
public class IntelPayConvertPlugin extends AbstractConvertPlugIn implements Plugin {
private static final Log logger = LogFactory.getLog(IntelPayConvertPlugin.class);
// 常量定义
private static final String INTEL_PAY_BILL = "bei_intelpay";//被动付款入账
private static final String MAPPING_BILL = "shjh_lszyykm";//摘要&会计科目映射表
private static final String ACCOUNT_VIEW = "bd_accountview";
private static final String TOLERANCE_AMOUNT = "0.01"; // 金额容差
@Override
public void afterConvert(AfterConvertEventArgs e) {
super.afterConvert(e);
try {
ExtendedDataEntitySet entitySet = e.getTargetExtDataEntitySet();
ExtendedDataEntity[] targetEntities = entitySet.FindByEntityKey(this.getTgtMainType().getName());
if (targetEntities.length == 0) {
logger.info("未找到目标单据实体,跳过处理");
return;
}
processConvert(targetEntities);
} catch (Exception ex) {
logger.error("单据转换处理过程中发生异常", ex);
throw new RuntimeException("转换处理失败", ex);
}
}
/**
* 处理转换逻辑
*/
private void processConvert(ExtendedDataEntity[] targetEntities) {
// 1. 收集源单ID
Set<Long> sourceBillIds = extractSourceBillIds(targetEntities);
// 2. 批量加载源单数据
Map<Long, DynamicObject> sourceBillMap = loadSourceBills(sourceBillIds);
if (sourceBillMap.isEmpty()) {
logger.warn("未找到对应的源单数据,跳过处理");
return;
}
// 3. 批量加载映射数据
Set<String> mappingBillNames = collectMappingBillNames(sourceBillMap.values());
Map<String, List<DynamicObject>> mappingBillCache = loadMappingBills(mappingBillNames);
// 4. 处理每个目标实体
for (ExtendedDataEntity targetEntity : targetEntities) {
processSingleEntity(targetEntity, sourceBillMap, mappingBillCache);
}
}
/**
* 提取源单ID集合
*/
private Set<Long> extractSourceBillIds(ExtendedDataEntity[] entities) {
return Arrays.stream(entities)
.map(entity -> entity.getDataEntity().getLong("sourcebillid"))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
/**
* 批量加载源单数据
*/
private Map<Long, DynamicObject> loadSourceBills(Set<Long> billIds) {
if (billIds.isEmpty()) {
return Collections.emptyMap();
}
String selectFields = "id,billno,description,rulename,company,debitamount";
return TmcDataServiceHelper.loadDataAndMapById(INTEL_PAY_BILL, selectFields, billIds);
}
/**
* 收集所有需要查询的映射单据名称
*/
private Set<String> collectMappingBillNames(Collection<DynamicObject> sourceBills) {
return sourceBills.stream()
.filter(this::isValidSourceBill)
.map(bill -> bill.getString("description"))
.filter(EmptyUtil::isNotEmpty)
.map(StrUtils::analysisSummary)
.filter(Objects::nonNull)
.flatMap(map -> map.keySet().stream())
.collect(Collectors.toSet());
}
/**
* 批量加载映射单据
*/
private Map<String, List<DynamicObject>> loadMappingBills(Set<String> mappingBillNames) {
if (mappingBillNames.isEmpty()) {
return Collections.emptyMap();
}
QFilter[] filters = {new QFilter("name", QCP.in, mappingBillNames.toArray())};
DynamicObject[] mappingBills = BusinessDataServiceHelper.load(
MAPPING_BILL, "id,name,shjh_kjkm,shjh_rate", filters);
return Arrays.stream(mappingBills)
.collect(Collectors.groupingBy(
bill -> bill.getString("name"),
Collectors.toList()
));
}
/**
* 处理单个目标实体
*/
private void processSingleEntity(ExtendedDataEntity targetEntity,
Map<Long, DynamicObject> sourceBillMap,
Map<String, List<DynamicObject>> mappingBillCache) {
DynamicObject targetData = targetEntity.getDataEntity();
Long sourceBillId = targetData.getLong("sourcebillid");
if (sourceBillId == 0L) {
logger.warn("目标单据缺少源单ID");
return;
}
DynamicObject sourceBill = sourceBillMap.get(sourceBillId);
if (sourceBill == null) {
logger.warn("未找到源单数据源单ID: {}", sourceBillId);
return;
}
if (!isProcessableSourceBill(sourceBill)) {
return;
}
try {
generatePaymentEntries(targetData, sourceBill, mappingBillCache);
} catch (Exception e) {
logger.error("生成付款分录失败,源单: {}", sourceBill.getString("billno"), e);
}
}
/**
* 检查源单是否可处理
*/
private boolean isProcessableSourceBill(DynamicObject sourceBill) {
String billNo = sourceBill.getString("billno");
String description = sourceBill.getString("description");
// 检查摘要是否为空
if (EmptyUtil.isEmpty(description)) {
logger.info("被动付款入账 {} 摘要为空,跳过处理", billNo);
return false;
}
// 检查映射功能是否开启
if (!StrUtils.isNeedGetMapBill(sourceBill)) {
logger.info("被动付款入账 {} 科目映射未开启,跳过处理", billNo);
return false;
}
return true;
}
/**
* 检查源单是否有效用于批量预处理
*/
private boolean isValidSourceBill(DynamicObject sourceBill) {
return EmptyUtil.isNotEmpty(sourceBill.getString("description"))
&& StrUtils.isNeedGetMapBill(sourceBill);
}
/**
* 生成付款分录
*/
private void generatePaymentEntries(DynamicObject targetData,
DynamicObject sourceBill,
Map<String, List<DynamicObject>> mappingBillCache) {
String description = sourceBill.getString("description");
BigDecimal debitAmount = sourceBill.getBigDecimal("debitamount");
Long sourceBillId = sourceBill.getLong("id");
// 解析摘要
Map<String, BigDecimal> amountMapping = StrUtils.analysisSummary(description);
if (amountMapping == null || amountMapping.isEmpty()) {
logger.error("摘要解析失败,源单: {},摘要: {}", sourceBill.getString("billno"), description);
return;
}
// 验证金额
if (!validateAmounts(amountMapping, debitAmount, sourceBill.getString("billno"))) {
return;
}
// 生成分录
DynamicObjectCollection paymentEntries = targetData.getDynamicObjectCollection("entry");
paymentEntries.clear();
BigDecimal allocatedAmount = createMappedEntries(paymentEntries, amountMapping,
sourceBill, mappingBillCache);
// 处理剩余金额
handleRemainingAmount(paymentEntries, allocatedAmount, debitAmount, sourceBill);
}
/**
* 验证金额合理性
*/
private boolean validateAmounts(Map<String, BigDecimal> amountMapping,
BigDecimal debitAmount, String billNo) {
if (EmptyUtil.isEmpty(debitAmount)) {
logger.error("付款金额为空,源单: {}", billNo);
return false;
}
BigDecimal amountSum = StrUtils.getAmountSum(amountMapping);
BigDecimal tolerance = new BigDecimal(TOLERANCE_AMOUNT);
// 允许一定的金额容差
if (amountSum.subtract(debitAmount).abs().compareTo(tolerance) > 0) {
logger.error("摘要金额之和与付款金额差异过大,源单: {},摘要金额: {},付款金额: {}",
billNo, amountSum, debitAmount);
return false;
}
return true;
}
/**
* 创建映射分录
*/
private BigDecimal createMappedEntries(DynamicObjectCollection paymentEntries,
Map<String, BigDecimal> amountMapping,
DynamicObject sourceBill,
Map<String, List<DynamicObject>> mappingBillCache) {
BigDecimal totalAllocated = BigDecimal.ZERO;
int sequence = 0;
for (Map.Entry<String, BigDecimal> entry : amountMapping.entrySet()) {
String mappingName = entry.getKey();
BigDecimal amount = entry.getValue();
List<DynamicObject> mappingBills = mappingBillCache.get(mappingName);
if (mappingBills == null || mappingBills.isEmpty()) {
logger.warn("未找到映射单据: {}", mappingName);
continue;
}
for (DynamicObject mappingBill : mappingBills) {
BigDecimal allocated = createSingleEntry(paymentEntries, sequence++,
sourceBill, mappingBill, amount);
totalAllocated = totalAllocated.add(allocated);
}
}
return totalAllocated;
}
/**
* 创建单个分录行
*/
private BigDecimal createSingleEntry(DynamicObjectCollection paymentEntries, int sequence,
DynamicObject sourceBill, DynamicObject mappingBill,
BigDecimal originalAmount) {
BigDecimal rate = mappingBill.getBigDecimal("shjh_rate");
if (rate == null) {
rate = BigDecimal.ONE;
}
String accountCode = mappingBill.getString("shjh_kjkm.number");
Long orgId = sourceBill.getLong("company.id");
DynamicObject account = loadAccount(accountCode, orgId);
if (account == null) {
logger.warn("未找到会计科目,编码: {},组织: {}", accountCode, orgId);
return BigDecimal.ZERO;
}
BigDecimal allocatedAmount = originalAmount.multiply(rate)
.setScale(2, RoundingMode.HALF_UP);
DynamicObject newEntry = paymentEntries.addNew();
newEntry.set("seq", sequence);
newEntry.set("e_sourcebillid", sourceBill.getLong("id"));
newEntry.set("e_sourcebillentryid", sourceBill.getLong("id"));
newEntry.set("shjh_sapkjkm", account);
newEntry.set("e_payableamt", allocatedAmount);
newEntry.set("e_actamt", allocatedAmount);
newEntry.set("e_remark", sourceBill.getString("rulename"));
newEntry.set("shjh_abstractadd", true);
TmcBotpHelper.addLinkEntity(newEntry, "entry", "sourcebilltype",
sourceBill, "e_sourcebillentryid");
return allocatedAmount;
}
/**
* 加载会计科目
*/
private DynamicObject loadAccount(String accountCode, Long orgId) {
QFilter[] filters = {
new QFilter("number", QCP.equals, accountCode),
new QFilter("org", QCP.equals, orgId)
};
try {
return BusinessDataServiceHelper.loadSingle(ACCOUNT_VIEW, filters);
} catch (Exception e) {
logger.error("加载会计科目失败,编码: {},组织: {}", accountCode, orgId, e);
return null;
}
}
/**
* 处理剩余金额
*/
private void handleRemainingAmount(DynamicObjectCollection paymentEntries,
BigDecimal allocatedAmount, BigDecimal debitAmount,
DynamicObject sourceBill) {
BigDecimal remaining = debitAmount.subtract(allocatedAmount);
// 只有剩余金额超过容差时才创建分录
if (remaining.abs().compareTo(new BigDecimal(TOLERANCE_AMOUNT)) > 0) {
int lastSeq = paymentEntries.size();
DynamicObject remainingEntry = paymentEntries.addNew();
remainingEntry.set("seq", lastSeq);
remainingEntry.set("e_sourcebillid", sourceBill.getLong("id"));
remainingEntry.set("e_sourcebillentryid", sourceBill.getLong("id"));
remainingEntry.set("e_payableamt", remaining);
remainingEntry.set("e_actamt", remaining);
remainingEntry.set("shjh_abstractadd", false);
TmcBotpHelper.addLinkEntity(remainingEntry, "entry", "sourcebilltype",
sourceBill, "e_sourcebillentryid");
logger.info("创建剩余金额分录,源单: {},剩余金额: {}",
sourceBill.getString("billno"), remaining);
}
}
}

View File

@ -92,18 +92,24 @@ public class PayBillSaveOperation extends AbstractOperationServicePlugIn impleme
DynamicObjectCollection entrys = bill.getDynamicObjectCollection("entry");//付款单分录 DynamicObjectCollection entrys = bill.getDynamicObjectCollection("entry");//付款单分录
if (null != entrys) { if (null != entrys) {
DynamicObject entryss = entrys.get(0);//首行(被动) for (DynamicObject entryObj : entrys) {
DynamicObject shjhYym = entryss.getDynamicObject("shjh_yym");//若原因码为空 boolean abstractLine= entryObj.getBoolean("shjh_abstractadd");
if (!abstractLine){
DynamicObject shjhYym = entryObj.getDynamicObject("shjh_yym");//若原因码为空
if (null == shjhYym) { if (null == shjhYym) {
DynamicObject shjhEBizsmall = entry.getDynamicObject("shjh_e_bizsmall");//规则--业务小类 DynamicObject shjhEBizsmall = entry.getDynamicObject("shjh_e_bizsmall");//规则--业务小类
if (null != shjhEBizsmall) { if (null != shjhEBizsmall) {
shjhEBizsmall = BusinessDataServiceHelper.loadSingle(shjhEBizsmall.getPkValue(),"shjh_bizsmalltype"); shjhEBizsmall = BusinessDataServiceHelper.loadSingle(shjhEBizsmall.getPkValue(),"shjh_bizsmalltype");
entryss.set("shjh_yym", shjhEBizsmall.getDynamicObject("shjh_yym"));//规则--小类--原因码 entryObj.set("shjh_yym", shjhEBizsmall.getDynamicObject("shjh_yym"));//规则--小类--原因码
entryss.set("shjh_sapkjkm", shjhEBizsmall.getDynamicObject("shjh_accountview"));//规则--小类--会计科目 entryObj.set("shjh_sapkjkm", shjhEBizsmall.getDynamicObject("shjh_accountview"));//规则--小类--会计科目
break;
} }
} }
} }
} }
//DynamicObject entryss = entrys.get(0);//首行(被动)
}
}
DynamicObject shjhCostcenter = (DynamicObject)bill.get("shjh_costcenter"); DynamicObject shjhCostcenter = (DynamicObject)bill.get("shjh_costcenter");
if (null == shjhCostcenter) { if (null == shjhCostcenter) {
bill.set("shjh_costcenter", entry.getDynamicObject("shjh_e_cc")); bill.set("shjh_costcenter", entry.getDynamicObject("shjh_e_cc"));

View File

@ -0,0 +1,169 @@
package shjh.jhzj7.fi.fi.utils;
import kd.bos.dataentity.entity.DynamicObject;
import kd.bos.dataentity.entity.DynamicObjectCollection;
import kd.bos.orm.query.QCP;
import kd.bos.orm.query.QFilter;
import kd.bos.servicehelper.BusinessDataServiceHelper;
import kd.tmc.fbp.common.util.EmptyUtil;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StrUtils {
/**
* 解析流水摘要中的中文和金额
*
* @param summary 流水摘要
* @return
* 缴棁,个人所得棁1212.50>("个人所得棁",1212.50)
* 代理国库税收收缴,11303250>("代理国库税收收缴",11303250)
* 缴税,生育保险费162.26|基本医疗保险费1460.34|生育保险费0.75|基本医疗保险费6.75|生育保险费1.50|基本医疗保险费13.50>("生育保险费",164.51)("基本医疗保险费",1480.59)
*/
public static Map<String, BigDecimal> analysisSummary(String summary) {
// 1.去除summary中的空格
String cleanedSummary = summary.replaceAll("\\s+", "");
// 如果字符串为空直接返回空map
if (cleanedSummary.isEmpty()) {
return new HashMap<>();
}
// 2.解析税种和金额
Map<String, BigDecimal> result = new HashMap<>();
// 特殊处理"代理国库税收收缴,数字"的格式
Pattern specialPattern = Pattern.compile("^(代理国库税收收缴),(\\d+(?:\\.\\d+)?)$");
Matcher specialMatcher = specialPattern.matcher(cleanedSummary);
if (specialMatcher.matches()) {
String taxName = specialMatcher.group(1);
String amountStr = specialMatcher.group(2);
try {
BigDecimal amount = new BigDecimal(amountStr);
result.put(taxName, amount);
return result;
} catch (NumberFormatException e) {
return null;
}
}
// 定义正则表达式匹配模式
// 模式1中文税种名称 + 数字整数或小数
// 模式2中文税种名称 + 分隔符 + 数字整数或小数
Pattern pattern = Pattern.compile("([\\u4e00-\\u9fa5]+)(?:[^\\d\\u4e00-\\u9fa5]*)(\\d+(?:\\.\\d+)?)");
Matcher matcher = pattern.matcher(cleanedSummary);
int lastMatchEnd = 0;
boolean validFormat = true;
while (matcher.find()) {
// 检查当前匹配是否紧接上一个匹配的结束位置
// 如果不是说明中间有不符合格式的内容
if (matcher.start() != lastMatchEnd) {
String between = cleanedSummary.substring(lastMatchEnd, matcher.start());
// 如果中间的内容包含数字说明格式不符合要求
if (between.matches(".*\\d.*")) {
validFormat = false;
break;
}
}
String taxName = matcher.group(1);
String amountStr = matcher.group(2);
try {
BigDecimal amount = new BigDecimal(amountStr);
// 合并相同税种的金额
if (result.containsKey(taxName)) {
BigDecimal existingAmount = result.get(taxName);
result.put(taxName, existingAmount.add(amount));
} else {
result.put(taxName, amount);
}
lastMatchEnd = matcher.end();
} catch (NumberFormatException e) {
validFormat = false;
break;
}
}
// 检查是否还有剩余内容未处理
if (validFormat && lastMatchEnd < cleanedSummary.length()) {
String remaining = cleanedSummary.substring(lastMatchEnd);
// 如果剩余内容包含数字说明格式不符合要求
if (remaining.matches(".*\\d.*")) {
validFormat = false;
}
}
// 如果格式不符合要求返回null
if (!validFormat || result.isEmpty()) {
return null;
}
return result;
}
/**
* 计算Map中所有金额的总和
* @param amountMap 包含金额的Map键为字符串值为BigDecimal
* @return 所有金额的总和如果Map为空或null则返回BigDecimal.ZERO
*/
public static BigDecimal getAmountSum(Map<String, BigDecimal> amountMap) {
if (amountMap == null || amountMap.isEmpty()) {
return BigDecimal.ZERO;
}
BigDecimal sum = BigDecimal.ZERO;
for (BigDecimal amount : amountMap.values()) {
if (amount != null) {
sum = sum.add(amount);
}
}
return sum;
}
/**
* 是否取税种&&科目映射按钮打开
* @return
*/
public static boolean isNeedGetMapBill(DynamicObject bill) {
boolean result=false;
String ruleName = bill.getString("rulename");
if (EmptyUtil.isEmpty(ruleName)){
return false;
}
//根据适配规则名称查询适配规则 (cas_recpayrule)
// 单据:生单/通知规则(shjh_cas_recpayrule_ext) 单据体:入账规则(entryentity) 字段:规则项名称(e_rulesname)
QFilter q2 = new QFilter("entryentity.e_rulesname", QCP.equals, ruleName);
QFilter q3 = new QFilter("enable", QCP.equals, "1");
q2 = q2.and(q3);
DynamicObject org = bill.getDynamicObject("company");
if (null != org) {
long orgid = org.getLong("id");
QFilter q4 = new QFilter("org_entry.u_org.id", QCP.equals, orgid);
q2 = q2.and(q4);
}
DynamicObject rule = BusinessDataServiceHelper.loadSingle("cas_recpayrule", q2.toArray());
if (null != rule) {
//根据适配规则携带对应分录的业务大类,业务小类,成本中心,利润中心
DynamicObjectCollection collection = rule.getDynamicObjectCollection("entryentity");
for (DynamicObject entry : collection) {
if (ruleName.equals(entry.getString("e_rulesname"))) {
result=entry.getBoolean("shjh_zykmys");
break;
}
}
}
return result;
}
}