diff --git a/main/java/shjh/jhzj7/fi/fi/plugin/convert/IntelPayConvertPlugin.java b/main/java/shjh/jhzj7/fi/fi/plugin/convert/IntelPayConvertPlugin.java new file mode 100644 index 0000000..2040297 --- /dev/null +++ b/main/java/shjh/jhzj7/fi/fi/plugin/convert/IntelPayConvertPlugin.java @@ -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 sourceBillIds = extractSourceBillIds(targetEntities); + + // 2. 批量加载源单数据 + Map sourceBillMap = loadSourceBills(sourceBillIds); + if (sourceBillMap.isEmpty()) { + logger.warn("未找到对应的源单数据,跳过处理"); + return; + } + + // 3. 批量加载映射数据 + Set mappingBillNames = collectMappingBillNames(sourceBillMap.values()); + Map> mappingBillCache = loadMappingBills(mappingBillNames); + + // 4. 处理每个目标实体 + for (ExtendedDataEntity targetEntity : targetEntities) { + processSingleEntity(targetEntity, sourceBillMap, mappingBillCache); + } + } + + /** + * 提取源单ID集合 + */ + private Set extractSourceBillIds(ExtendedDataEntity[] entities) { + return Arrays.stream(entities) + .map(entity -> entity.getDataEntity().getLong("sourcebillid")) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + /** + * 批量加载源单数据 + */ + private Map loadSourceBills(Set 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 collectMappingBillNames(Collection 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> loadMappingBills(Set 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 sourceBillMap, + Map> 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> mappingBillCache) { + String description = sourceBill.getString("description"); + BigDecimal debitAmount = sourceBill.getBigDecimal("debitamount"); + Long sourceBillId = sourceBill.getLong("id"); + + // 解析摘要 + Map 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 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 amountMapping, + DynamicObject sourceBill, + Map> mappingBillCache) { + BigDecimal totalAllocated = BigDecimal.ZERO; + int sequence = 0; + + for (Map.Entry entry : amountMapping.entrySet()) { + String mappingName = entry.getKey(); + BigDecimal amount = entry.getValue(); + + List 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); + } + } +} \ No newline at end of file diff --git a/main/java/shjh/jhzj7/fi/fi/plugin/operate/PayBillSaveOperation.java b/main/java/shjh/jhzj7/fi/fi/plugin/operate/PayBillSaveOperation.java index 3c02203..bd344b6 100644 --- a/main/java/shjh/jhzj7/fi/fi/plugin/operate/PayBillSaveOperation.java +++ b/main/java/shjh/jhzj7/fi/fi/plugin/operate/PayBillSaveOperation.java @@ -92,16 +92,22 @@ public class PayBillSaveOperation extends AbstractOperationServicePlugIn impleme DynamicObjectCollection entrys = bill.getDynamicObjectCollection("entry");//付款单分录 if (null != entrys) { - DynamicObject entryss = entrys.get(0);//首行(被动) - DynamicObject shjhYym = entryss.getDynamicObject("shjh_yym");//若原因码为空 - if (null == shjhYym) { - DynamicObject shjhEBizsmall = entry.getDynamicObject("shjh_e_bizsmall");//规则--业务小类 - if (null != shjhEBizsmall) { - shjhEBizsmall = BusinessDataServiceHelper.loadSingle(shjhEBizsmall.getPkValue(),"shjh_bizsmalltype"); - entryss.set("shjh_yym", shjhEBizsmall.getDynamicObject("shjh_yym"));//规则--小类--原因码 - entryss.set("shjh_sapkjkm", shjhEBizsmall.getDynamicObject("shjh_accountview"));//规则--小类--会计科目 - } + for (DynamicObject entryObj : entrys) { + boolean abstractLine= entryObj.getBoolean("shjh_abstractadd"); + if (!abstractLine){ + DynamicObject shjhYym = entryObj.getDynamicObject("shjh_yym");//若原因码为空 + if (null == shjhYym) { + DynamicObject shjhEBizsmall = entry.getDynamicObject("shjh_e_bizsmall");//规则--业务小类 + if (null != shjhEBizsmall) { + shjhEBizsmall = BusinessDataServiceHelper.loadSingle(shjhEBizsmall.getPkValue(),"shjh_bizsmalltype"); + entryObj.set("shjh_yym", shjhEBizsmall.getDynamicObject("shjh_yym"));//规则--小类--原因码 + entryObj.set("shjh_sapkjkm", shjhEBizsmall.getDynamicObject("shjh_accountview"));//规则--小类--会计科目 + break; + } + } + } } + //DynamicObject entryss = entrys.get(0);//首行(被动) } } DynamicObject shjhCostcenter = (DynamicObject)bill.get("shjh_costcenter"); diff --git a/main/java/shjh/jhzj7/fi/fi/utils/StrUtils.java b/main/java/shjh/jhzj7/fi/fi/utils/StrUtils.java new file mode 100644 index 0000000..2a69a6f --- /dev/null +++ b/main/java/shjh/jhzj7/fi/fi/utils/StrUtils.java @@ -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 analysisSummary(String summary) { + // 1.去除summary中的空格 + String cleanedSummary = summary.replaceAll("\\s+", ""); + + // 如果字符串为空,直接返回空map + if (cleanedSummary.isEmpty()) { + return new HashMap<>(); + } + + // 2.解析税种和金额 + Map 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 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; + } + + +} \ No newline at end of file