Эх сурвалжийг харах

1.个人额度同步后台任务

lisheng 1 долоо хоног өмнө
parent
commit
be6d842f18

+ 30 - 0
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/common/SyncStatusEnum.java

@@ -0,0 +1,30 @@
+package nckd.jimin.jyyy.fi.common;
+
+public enum SyncStatusEnum {
+    UNSYNC("待同步","10"),
+    SYNCING("同步中","20"),
+    SYNCFAIL("同步失败","30"),
+    SYNCSUCCESS("同步成功","40");
+    private String  name;
+    private String  value;
+
+    SyncStatusEnum(String name, String value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    public String getName() {
+        return name;
+    }
+    public static String getByValue(String value) {
+        for (SyncStatusEnum e : values()) {
+            if (e.getValue().equals(value)) {
+                return e.getName();
+            }
+        }
+        return null;
+    }
+    public String getValue() {
+        return value;
+    }
+}

+ 15 - 6
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/common/constant/BillTypeConstants.java

@@ -2,12 +2,6 @@ package nckd.jimin.jyyy.fi.common.constant;
 
 public interface BillTypeConstants {
 
-    String HRPI_PERSON = "hrpi_person";
-
-    String HRPI_BASELOCATION = "hrpi_baselocation";
-
-    String HBSS_WORKPLACE = "hbss_workplace";
-
     String GL_VOUCHER = "gl_voucher";
 
     String CAS_PAYBILL = "cas_paybill";
@@ -46,4 +40,19 @@ public interface BillTypeConstants {
     String ER_REPAYMENTBILL = "er_repaymentbill";
 
     String ER_REIMBURSEAMOUNT = "er_reimburseamount";
+
+    /**
+     * 职级职等
+     */
+    String HRPI_EMPJOBREL = "hrpi_empjobrel";
+    /**
+     * 任职经历
+     */
+    String HRPI_EMPPOSORGREL = "hrpi_empposorgrel";
+
+    String HRPI_PERSON = "hrpi_person";
+
+    String HRPI_BASELOCATION = "hrpi_baselocation";
+
+    String HBSS_WORKPLACE = "hbss_workplace";
 }

+ 31 - 0
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/common/constant/LimitRelasetConstant.java

@@ -0,0 +1,31 @@
+package nckd.jimin.jyyy.fi.common.constant;
+
+public interface LimitRelasetConstant extends BillConstant{
+
+    /**
+     * 单据标识
+     */
+    String ENTITYID = "nckd_limitrelaset";
+    /**
+     * 费用项目
+     */
+    String KEY_NCKD_EXPENSEITEM = "nckd_expenseitem";
+    /**
+     * 职位
+     */
+    String KEY_NCKD_JOBLEVELHR = "nckd_joblevelhr";
+    /**
+     * 岗位
+     */
+    String KEY_NCKD_POSITIONHR = "nckd_positionhr";
+    /**
+     * 额度
+     */
+    String KEY_NCKD_AMOUNT = "nckd_amount";
+
+    /**
+     * 币别
+     */
+    String KEY_NCKD_CURRENCY = "nckd_currency";
+
+}

+ 99 - 0
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/common/constant/PersonReimSyncTaskConstant.java

@@ -0,0 +1,99 @@
+package nckd.jimin.jyyy.fi.common.constant;
+
+public interface PersonReimSyncTaskConstant extends BillConstant{
+
+    /**
+     * 单据标识
+     */
+    String ENTITYID = "nckd_personreimsynctask";
+    /**
+     * 同步人员
+     */
+    String KEY_NCKD_USER = "nckd_user";
+    /**
+     * 变更后组织
+     */
+    String KEY_NCKD_ORG = "nckd_org";
+
+    /**
+     * 变更前组织
+     */
+    String KEY_NCKD_ORG_PRE = "nckd_org_pre";
+    /**
+     * 变更后岗位
+     */
+    String KEY_NCKD_POSITIONHR = "nckd_positionhr";
+    /**
+     * 变更后职位
+     */
+    String KEY_NCKD_JOBLEVELHR = "nckd_joblevelhr";
+    /**
+     * 变更前岗位
+     */
+    String KEY_NCKD_POSITIONHR_PRE = "nckd_positionhr_pre";
+    /**
+     * 变更前职位
+     */
+    String KEY_NCKD_JOBLEVELHR_PRE = "nckd_joblevelhr_pre";
+    /**
+     * 个人额度同步状态
+     */
+    String KEY_NCKD_REIM_SYNCSTATUS = "nckd_reim_syncstatus";
+    /**
+     * 同步年
+     */
+    String KEY_NCKD_YEAR = "nckd_year";
+    /**
+     * 同步月
+     */
+    String KEY_NCKD_MONTH = "nckd_month";
+
+
+
+    /**
+     * 报销级别同步状态
+     */
+    String KEY_NCKD_SYNCSTATUS = "nckd_syncstatus";
+    /**
+     * 来源单据ID
+     */
+    String KEY_NCKD_SOURCEBILLID = "nckd_sourcebillid";
+
+    interface NCKD_RECORDENTRY{
+        String ENTITYID = "nckd_recordentry";
+
+        /**
+         * 同步时间
+         */
+        String KEY_NCKD_SYNC_DATE = "nckd_sync_date";
+        /**
+         * 是否成功
+         */
+        String KEY_NCKD_SYNC_ISSUCCESS = "nckd_sync_issuccess";
+        /**
+         * 同步信息
+         */
+        String KEY_NCKD_SYNC_MSG = "nckd_sync_msg";
+        /**
+         * traceid
+         */
+        String KEY_NCKD_TRACEID = "nckd_traceid";
+
+        String KEY_NCKD_SYNCTYPE = "nckd_synctype";
+
+    }
+
+    interface OPERATE{
+
+        /**
+         * 同步个人额度表
+         */
+        String SYNCPERSONREIM = "syncpersonreim";
+
+        /**
+         * 同步差旅报销级别
+         */
+        String SYNCREIMLEVER = "syncreimlever";
+
+    }
+}

+ 40 - 4
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/common/util/CommonUtils.java

@@ -3,12 +3,13 @@ package nckd.jimin.jyyy.fi.common.util;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import kd.bos.algo.DataSet;
+import kd.bos.dataentity.utils.StringUtils;
+import kd.bos.entity.operate.result.IOperateInfo;
+import kd.bos.entity.operate.result.OperateErrorInfo;
+import kd.bos.entity.operate.result.OperationResult;
 
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 public class CommonUtils {
@@ -60,4 +61,39 @@ public class CommonUtils {
         }
         return dataSet;
     }
+
+    /**
+     * 获取操作错误信息
+     * @param operationResult
+     * @return
+     */
+    public static String getOperationResultErrorInfos(OperationResult operationResult){
+        if(operationResult.isSuccess()){
+            return StringUtils.EMPTY;
+        }
+
+        List<IOperateInfo> errorInfos = operationResult.getAllErrorOrValidateInfo();
+        int size = errorInfos.size() + operationResult.getSuccessPkIds().size();
+        if (size > 1) {
+            StringBuilder stringBuilder = new StringBuilder();
+            int i = 0;
+            for(int len = errorInfos.size(); i < 5 && i < len; ++i) {
+                stringBuilder.append((errorInfos.get(i)).getMessage());
+            }
+            return stringBuilder.toString();
+        } else if (!errorInfos.isEmpty()) {
+            OperateErrorInfo errorInfo = (OperateErrorInfo)errorInfos.get(0);
+            String msg = errorInfo.getMessage() == null ? "" : errorInfo.getMessage();
+            return msg;
+        } else{
+            String msg = operationResult.getMessage() == null ? "" : operationResult.getMessage();
+            return msg;
+        }
+    }
+
+    public static int getDateFeild(Date date , int filed){
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        return calendar.get(filed);
+    }
 }

+ 348 - 0
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/plugin/operate/PersonReimSyncTaskOp.java

@@ -0,0 +1,348 @@
+package nckd.jimin.jyyy.fi.plugin.operate;
+
+import com.google.common.collect.Lists;
+import kd.bos.context.RequestContext;
+import kd.bos.dataentity.OperateOption;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.dataentity.utils.StringUtils;
+import kd.bos.db.tx.TX;
+import kd.bos.db.tx.TXHandle;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.operate.result.OperateErrorInfo;
+import kd.bos.entity.operate.result.OperationResult;
+import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.PreparePropertysEventArgs;
+import kd.bos.entity.plugin.args.AfterOperationArgs;
+import kd.bos.entity.validate.ErrorLevel;
+import kd.bos.exception.KDBizException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.org.utils.DynamicObjectUtils;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import kd.bos.servicehelper.operation.OperationServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.fi.gl.util.BigDecimalUtil;
+import nckd.jimin.jyyy.fi.common.constant.BillTypeConstants;
+import nckd.jimin.jyyy.fi.common.constant.LimitRelasetConstant;
+import nckd.jimin.jyyy.fi.common.constant.PersonReimSyncTaskConstant;
+import nckd.jimin.jyyy.fi.common.util.CommonUtils;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class PersonReimSyncTaskOp extends AbstractOperationServicePlugIn {
+    private static final Log logger = LogFactory.getLog(PersonReimSyncTaskOp.class);
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        super.onPreparePropertys(e);
+    }
+
+    @Override
+    public void afterExecuteOperationTransaction(AfterOperationArgs e) {
+        super.afterExecuteOperationTransaction(e);
+        DynamicObject[] dataEntities = e.getDataEntities();
+        String operationKey = e.getOperationKey();
+
+        List<Object> billIdList = Arrays.stream(dataEntities).map(r -> r.get(PersonReimSyncTaskConstant.ID)).collect(Collectors.toList());
+        // 同步个人额度
+        if(PersonReimSyncTaskConstant.OPERATE.SYNCPERSONREIM.equals(operationKey)){
+            List<List<Object>> patchList = Lists.partition(billIdList, 50);
+            for(List<Object> patchBillList : patchList){
+                DynamicObject[] billDatas = BusinessDataServiceHelper.load(patchBillList.toArray(), EntityMetadataCache.getDataEntityType(PersonReimSyncTaskConstant.ENTITYID));
+                Arrays.stream(billDatas).forEach(r -> doSyncPersonReim(r));
+            }
+        }
+
+    }
+    protected void doSyncPersonReim(DynamicObject billInfo){
+        // 校验数据是否合格
+        DynamicObject user = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_USER);
+        DynamicObject org = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_ORG);
+        DynamicObject preOrg = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_ORG_PRE);
+        DynamicObject position = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR);
+        DynamicObject prePosition = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR_PRE);
+        DynamicObject jobLever = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR);
+        DynamicObject preJobLever = billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR_PRE);
+
+        int year = billInfo.getInt(PersonReimSyncTaskConstant.KEY_NCKD_YEAR);
+        // 增加事务控制,如果发生异常,回滚生成的额度信息,保持数据一致
+        try(TXHandle h = TX.requiresNew(getClass().getName()+"doSyncPersonReim")){
+            try {
+                // 校验数据
+                checkData(billInfo);
+                // 获取总额度  费用项目、职级、岗位
+                Map<Long, Map<Long, BigDecimal>> currencyReimJobAmount = getExpenseJobAmount(position.getPkValue(), jobLever.getPkValue());
+                // 判断组织是否存在便变更,组织变更则作废原组织
+                if(!org.getPkValue().equals(preOrg.getPkValue())){
+                    doInvalidPersonReim(user.getLong(PersonReimSyncTaskConstant.ID),preOrg.getLong(PersonReimSyncTaskConstant.ID),null,null,year);
+                }
+
+                // 变更前岗位、职位为空,直接新增
+                if(prePosition == null || preJobLever == null){
+                    createPersonReimDirect(billInfo,currencyReimJobAmount);
+                }else{
+                    Map<Long, Map<Long, BigDecimal>> sourceCurrencyReimJobAmount = getExpenseJobAmount(prePosition.getPkValue(), preJobLever.getPkValue());
+                    Set<Long> invalidCurrencySet = sourceCurrencyReimJobAmount.keySet().stream()
+                            .filter(r -> !currencyReimJobAmount.keySet().contains(r)).collect(Collectors.toSet());
+                    // 作废已删除的币别数据
+                    logger.info(String.format("需要作废的币别ID为:" + StringUtils.join(",",invalidCurrencySet)));
+                    invalidCurrencySet.forEach(r -> doInvalidPersonReim(user.getLong(PersonReimSyncTaskConstant.ID),org.getLong(PersonReimSyncTaskConstant.ID),null,r,year));
+                    for(Map.Entry<Long, Map<Long, BigDecimal>> currencyRow : currencyReimJobAmount.entrySet()){
+                        Long currencyId = currencyRow.getKey();
+                        Map<Long, BigDecimal> expenseJobAmount = currencyRow.getValue();
+                        if(sourceCurrencyReimJobAmount.containsKey(currencyId)){
+                            Set<Long> invalidExpenseSet = sourceCurrencyReimJobAmount.get(currencyId).keySet().stream()
+                                    .filter(r -> !expenseJobAmount.keySet().contains(r)).collect(Collectors.toSet());
+                            logger.info(String.format("需要作废的费用类型ID为:" + StringUtils.join(",",invalidExpenseSet)));
+                            invalidExpenseSet.forEach(r -> doInvalidPersonReim(user.getLong(PersonReimSyncTaskConstant.ID),org.getLong(PersonReimSyncTaskConstant.ID),r,currencyId,year));
+                        }
+                        for(Map.Entry<Long, BigDecimal> dataRow : expenseJobAmount.entrySet()){
+                            Long expenseItemId = dataRow.getKey();
+                            BigDecimal amount = dataRow.getValue();
+                            createPersonReimByExpense(billInfo,currencyId,expenseItemId,amount);
+                        }
+                    }
+                }
+                addRecord(billInfo,"创建成功。",Boolean.TRUE);
+            }catch (Exception e){
+                logger.error(String.format("单据%s同步额度失败",billInfo.getString(PersonReimSyncTaskConstant.KEY_NUMBER)),e);
+                String errorMsg = StringUtils.isEmpty(e.getMessage())?"同步额度时发生未知异常,请用traceid在monitor通过任务编码查看日志详情。":e.getMessage();
+                addRecord(billInfo,errorMsg,Boolean.FALSE);
+                h.markRollback();
+            }
+        }
+        SaveServiceHelper.save(new DynamicObject[]{billInfo});
+    }
+
+    /**
+     * 直接创建个人额度信息
+     * @param billInfo
+     * @param currencyReimJobAmount
+     */
+    protected void createPersonReimDirect(DynamicObject billInfo,Map<Long, Map<Long, BigDecimal>> currencyReimJobAmount){
+        for(Map.Entry<Long, Map<Long, BigDecimal>> currencyRow : currencyReimJobAmount.entrySet()){
+            Long currencyId = currencyRow.getKey();
+            Map<Long, BigDecimal> expenseJobAmount = currencyRow.getValue();
+            for(Map.Entry<Long, BigDecimal> dataRow : expenseJobAmount.entrySet()){
+                Long expenseItemId = dataRow.getKey();
+                BigDecimal amount = dataRow.getValue();
+                createPersonReimByExpense(billInfo,currencyId,expenseItemId,amount);
+            }
+        }
+    }
+
+    protected void createPersonReimByExpense(DynamicObject billInfo,Long currencyId,Long expenseItemId,BigDecimal amount){
+        // 创建个人额度表
+        DynamicObject personReim = createPersonReim(
+                billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_USER).getLong(PersonReimSyncTaskConstant.ID),
+                billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_ORG).getLong(PersonReimSyncTaskConstant.ID),
+                expenseItemId,
+                currencyId,amount,billInfo);
+        // 调用审核
+        OperationResult saveOp = OperationServiceHelper.executeOperate("save", BillTypeConstants.ER_REIMBURSEAMOUNT, new DynamicObject[]{personReim}, OperateOption.create());
+        if(!saveOp.isSuccess() || saveOp.getSuccessPkIds().size() == 0){
+            throw new KDBizException(CommonUtils.getOperationResultErrorInfos(saveOp));
+        }
+        OperationResult approveOp = OperationServiceHelper.executeOperate("approve", BillTypeConstants.ER_REIMBURSEAMOUNT, new Object[]{saveOp.getSuccessPkIds().get(0)}, OperateOption.create());
+        if(!approveOp.isSuccess() || approveOp.getSuccessPkIds().size() == 0){
+            throw new KDBizException(CommonUtils.getOperationResultErrorInfos(approveOp));
+        }
+    }
+
+
+    protected void checkData(DynamicObject billInfo){
+        if(billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_USER) == null){
+            throw new KDBizException("同步人员不能为空。");
+        }
+        if(billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_ORG) == null){
+            throw new KDBizException("组织不能为空。");
+        }
+        if(billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR) == null){
+            throw new KDBizException("岗位人员不能为空。");
+        }
+        if(billInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR) == null){
+            throw new KDBizException("职级不能为空。");
+        }
+    }
+
+    /**
+     * 作废条件范围内的个人额度信息
+     * @param userId
+     * @param companyId
+     * @param expenseItemId
+     * @param currencyId
+     */
+    protected void doInvalidPersonReim(Long userId , Long companyId , Long expenseItemId ,Long currencyId , int year){
+        // 作废币别下
+        List<QFilter> filterList = new ArrayList<>();
+        if(userId != null && userId == 0L){
+            filterList.add(new QFilter("employee.id", QCP.equals, userId));
+            if(companyId != null && companyId != 0L){
+                filterList.add(new QFilter("company.id", QCP.equals, companyId));
+            }
+            if(expenseItemId != null && expenseItemId != 0L){
+                filterList.add(new QFilter("expenseitem.id", QCP.equals, expenseItemId));
+            }
+            filterList.add(new QFilter("currency.id", QCP.equals, currencyId));
+            filterList.add(new QFilter("auditstatus", QCP.equals, "1"));
+            filterList.add(new QFilter("dateyear", QCP.equals, String.valueOf(year)));
+            DynamicObject[] invalidPersonReimArray = BusinessDataServiceHelper.load(BillTypeConstants.ER_REIMBURSEAMOUNT, "auditstatus", filterList.toArray(new QFilter[0]));
+            for(DynamicObject invalidPersonReim : invalidPersonReimArray){
+                invalidPersonReim.set("auditstatus","2");
+            }
+            SaveServiceHelper.save(invalidPersonReimArray);
+        }
+    }
+
+
+    /**
+     * 获取个人额度同步表 创建某个费用类型的个人额度
+     * @param userId
+     * @param companyId
+     * @param expenseItemId
+     * @param currencyId
+     * @param amount
+     * @param billInfo
+     * @return
+     */
+    protected DynamicObject createPersonReim(Long userId , Long companyId , Long expenseItemId ,Long currencyId,
+                                             BigDecimal amount , DynamicObject billInfo){
+
+        int year = billInfo.getInt(PersonReimSyncTaskConstant.KEY_NCKD_YEAR);
+
+        int month = billInfo.getInt(PersonReimSyncTaskConstant.KEY_NCKD_MONTH);
+
+        Date now = new Date();
+        long currUserId = RequestContext.get().getCurrUserId();
+
+        // 已与实施确认,通过公司、费用类型、人员、币别、年度确认是否已经创建个人额度表;如果没有则新增,存在则修改
+        DynamicObject queryPersonAmount = QueryServiceHelper.queryOne(BillTypeConstants.ER_REIMBURSEAMOUNT, "id", new QFilter[]{
+                new QFilter("company.id", QCP.equals, companyId),
+                new QFilter("employee.id", QCP.equals, userId),
+                new QFilter("expenseitem.id", QCP.equals, expenseItemId),
+                new QFilter("currency.id", QCP.equals, currencyId),
+                new QFilter("auditstatus", QCP.equals, "1"),
+                new QFilter("dateyear", QCP.equals, String.valueOf(year))
+        });
+        DynamicObject personAmountBill = BusinessDataServiceHelper.newDynamicObject(BillTypeConstants.ER_REIMBURSEAMOUNT);
+        // 如果存在则从个人额度中复制
+        if(queryPersonAmount != null){
+            DynamicObject oriPersonAmount = BusinessDataServiceHelper.loadSingle(queryPersonAmount.get("id"), BillTypeConstants.ER_REIMBURSEAMOUNT);
+            Set<String> ignoreSet = new HashSet<>(Arrays.asList("id"));
+            DynamicObjectUtils.copy(oriPersonAmount,personAmountBill);
+            // 标准字段
+            personAmountBill.set("wbsrcbilltype","er_reimctl_modify");
+            personAmountBill.set("wbsrcbillid",oriPersonAmount.getPkValue());
+            setPersonAmountData(personAmountBill,amount,month);
+        }else{
+            DynamicObject userInfo = BusinessDataServiceHelper.loadSingleFromCache(userId, "bos_user");
+            DynamicObject companyInfo = BusinessDataServiceHelper.loadSingleFromCache(companyId, "bos_org");
+            DynamicObject expenseItemInfo = BusinessDataServiceHelper.loadSingleFromCache(expenseItemId, "er_expenseitemedit");
+            DynamicObject currencyInfo = BusinessDataServiceHelper.loadSingleFromCache(currencyId, "bd_currency");
+
+            personAmountBill.set("company", companyInfo);
+            personAmountBill.set("expenseitem", expenseItemInfo);
+            personAmountBill.set("currency", currencyInfo);
+            personAmountBill.set("dateyear", String.valueOf(year));
+            personAmountBill.set("costcompany", companyId);
+
+            personAmountBill.set("amounttype", "1");
+            personAmountBill.set("employee", userInfo);
+            //personAmountBill.set("dept", reimburseQuota.get("dept.id"));
+            personAmountBill.set("auditstatus", "0");
+            personAmountBill.set("createtime", now);
+            personAmountBill.set("modifytime", now);
+            personAmountBill.set("creator", currUserId);
+            personAmountBill.set("modifier", currUserId);
+            personAmountBill.set("wbsrcbilltype", "er_reimctl_new");
+            setPersonAmountData(personAmountBill,amount,1);
+        }
+
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        // 计算年总额度
+        for(int i = 1; i <= 12; i++) {
+            totalAmount = BigDecimalUtil.add(totalAmount,personAmountBill.getBigDecimal("month" + i));
+        }
+        personAmountBill.set("totalamount", totalAmount);
+        personAmountBill.set("auditstatus", "0");
+
+        // 岗位新增变动:positionchange ;年度复制:yearcopy  ; 新增字段,与标准字段区分,防止与标品逻辑冲突
+        personAmountBill.set("nckd_sourcetype","positionchange");
+        personAmountBill.set("nckd_sourcebillid",billInfo.getPkValue());
+
+        return personAmountBill;
+    }
+
+    /**
+     * 对个人额度表的额度字段赋值
+     * @param personAmountBill 个人额度表
+     * @param amount 额度
+     * @param startMonth 开始月份,新单则从0开始,非新单则从指定月份开始
+     */
+    protected void setPersonAmountData(DynamicObject personAmountBill , BigDecimal amount , int startMonth){
+
+        for(int i = startMonth; i <= 12; i++) {
+
+            personAmountBill.set("month" + i, amount);
+
+        }
+
+    }
+
+
+    protected void addRecord(DynamicObject billInfo , String successMsg , Boolean isSuccess){
+        DynamicObjectCollection recordEntryCol = billInfo.getDynamicObjectCollection(PersonReimSyncTaskConstant.NCKD_RECORDENTRY.ENTITYID);
+        DynamicObject record = recordEntryCol.addNew();
+        record.set(PersonReimSyncTaskConstant.NCKD_RECORDENTRY.KEY_NCKD_SYNC_DATE,new Date());
+        record.set(PersonReimSyncTaskConstant.NCKD_RECORDENTRY.KEY_NCKD_TRACEID, RequestContext.get().getTraceId());
+        record.set(PersonReimSyncTaskConstant.NCKD_RECORDENTRY.KEY_NCKD_SYNC_ISSUCCESS,isSuccess?1:0);
+        record.set(PersonReimSyncTaskConstant.NCKD_RECORDENTRY.KEY_NCKD_SYNC_MSG, StringUtils.substring(successMsg,0,500));
+
+        billInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_SYNCSTATUS,isSuccess?"40":"30");
+
+        if(!isSuccess){
+            OperationResult operationResult = this.getOperationResult();
+            operationResult.setSuccess(false);
+            operationResult.getSuccessPkIds().removeIf(r -> billInfo.getPkValue().equals(r));
+            OperateErrorInfo operateErrorInfo = new OperateErrorInfo();
+            operateErrorInfo.setMessage(successMsg);
+            operateErrorInfo.setLevel(ErrorLevel.Error);
+            operationResult.addErrorInfo(operateErrorInfo);
+        }
+
+
+    }
+
+    /**
+     * 通过 费用项目与职级岗位关系配置 获取额度信息
+     * 按照币别分组,允许设置不同币别的方案
+     * @return
+     */
+    protected Map<Long, Map<Long, BigDecimal>> getExpenseJobAmount(Object positionId , Object jobLeverId){
+
+        StringJoiner joiner = new StringJoiner(",");
+        joiner.add(LimitRelasetConstant.KEY_NCKD_CURRENCY);
+        joiner.add(LimitRelasetConstant.KEY_NCKD_EXPENSEITEM);
+        joiner.add(LimitRelasetConstant.KEY_NCKD_AMOUNT);
+
+        DynamicObject[] limitRelasetArray = BusinessDataServiceHelper.load(LimitRelasetConstant.ENTITYID, joiner.toString(), new QFilter[]{
+                new QFilter(LimitRelasetConstant.KEY_NCKD_POSITIONHR, QCP.equals, positionId),
+                new QFilter(LimitRelasetConstant.KEY_NCKD_JOBLEVELHR, QCP.equals, jobLeverId),
+                new QFilter(LimitRelasetConstant.KEY_ENABLE, QCP.equals, "1"),
+                new QFilter(LimitRelasetConstant.KEY_STATUS, QCP.equals, "C")
+        });
+        if(limitRelasetArray == null || limitRelasetArray.length == 0){
+            return new HashMap<>();
+        }
+        return Arrays.stream(limitRelasetArray)
+                .filter(r -> r.getDynamicObject(LimitRelasetConstant.KEY_NCKD_EXPENSEITEM) != null)
+                .collect(Collectors.groupingBy(r -> r.getDynamicObject(LimitRelasetConstant.KEY_NCKD_CURRENCY).getLong(PersonReimSyncTaskConstant.ID),
+                        Collectors.toMap(r -> r.getDynamicObject(LimitRelasetConstant.KEY_NCKD_EXPENSEITEM).getLong(PersonReimSyncTaskConstant.ID),
+                                r -> r.getBigDecimal(LimitRelasetConstant.KEY_NCKD_AMOUNT), (a, b) -> a)));
+    }
+}

+ 270 - 0
code/jyyy/nckd-jimin-jyyy-fi/src/main/java/nckd/jimin/jyyy/fi/task/PersonReimSyncTask.java

@@ -0,0 +1,270 @@
+package nckd.jimin.jyyy.fi.task;
+
+import com.google.common.collect.Lists;
+import kd.bos.context.RequestContext;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.exception.KDBizException;
+import kd.bos.exception.KDException;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.login.utils.DateUtils;
+import kd.bos.orm.ORM;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.schedule.api.StopTask;
+import kd.bos.schedule.executor.AbstractTask;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.bos.util.CollectionUtils;
+import kd.bos.util.StringUtils;
+import nckd.jimin.jyyy.fi.common.SyncStatusEnum;
+import nckd.jimin.jyyy.fi.common.constant.BillTypeConstants;
+import nckd.jimin.jyyy.fi.common.constant.PersonReimSyncTaskConstant;
+import nckd.jimin.jyyy.fi.common.util.CommonUtils;
+
+import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 个人额度更新功能
+ */
+public class PersonReimSyncTask extends AbstractTask implements StopTask {
+
+    private static final Log logger = LogFactory.getLog(PersonReimSyncTask.class);
+
+    @Override
+    public void execute(RequestContext requestContext, Map<String, Object> map) throws KDException {
+        logger.info("map data {}" , map);
+        Date beginDate = getBeginDate(map);
+        Date endDate = getEndDate(map);
+        syncMonthPersonReim(beginDate,endDate);
+    }
+
+    /**
+     * 1.参数中有值取参数
+     * 2.参数没有值则默认取上个月第一天
+     * @param map
+     * @return
+     */
+    protected Date getBeginDate(Map<String, Object> map){
+        if(map.containsKey("beginDate") && StringUtils.isNotEmpty((String)map.get("beginDate"))){
+            String beginDateStr = (String)map.get("beginDate");
+            return DateUtils.parseDate(beginDateStr);
+        }else{
+            LocalDate today = LocalDate.now();
+            // 获取上个月的YearMonth对象
+            YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
+            // 获取上个月的第一天
+            LocalDate firstDayOfLastMonth = lastMonth.atDay(1);
+            // 转换为Date对象(默认时区)
+            return Date.from(firstDayOfLastMonth.atStartOfDay()
+                    .atZone(ZoneId.systemDefault())
+                    .toInstant());
+        }
+    }
+    /**
+     * 1.参数中有值取参数
+     * 2.参数没有值则默认取上个月最后一天
+     * @param map
+     * @return
+     */
+    protected Date getEndDate(Map<String, Object> map){
+        if(map.containsKey("endDate") && StringUtils.isNotEmpty((String)map.get("endDate"))){
+            String endDateStr = (String)map.get("endDate");
+            return DateUtils.parseDate(endDateStr);
+        }else{
+            // 获取当前日期
+            LocalDate today = LocalDate.now();
+
+            // 获取上个月的最后一天
+            LocalDate lastDayOfLastMonth = today.withDayOfMonth(1).minusDays(1);
+
+            // 转换为Date对象(默认时区)
+            return Date.from(lastDayOfLastMonth.atStartOfDay()
+                    .atZone(ZoneId.systemDefault())
+                    .toInstant());
+        }
+    }
+
+
+    /**
+     *  查询指定时间范围存在职位变更的人员信息,创建个人额度
+     * @param beginDate 开始时间
+     * @param endDate 结束时间
+     */
+    protected void syncMonthPersonReim(Date beginDate , Date endDate){
+
+        // 从任职经历中查询岗位发生变化的数据
+        Set<Object> positionPersonSet = QueryServiceHelper.query(BillTypeConstants.HRPI_EMPPOSORGREL, "person.id", new QFilter[]{
+                new QFilter("startdate", QCP.large_equals, beginDate),
+                new QFilter("startdate", QCP.less_equals, endDate),
+                new QFilter("businessstatus", QCP.less_equals, "1"),
+                new QFilter("iscurrentversion", QCP.less_equals, "1"),
+                new QFilter("isprimary", QCP.less_equals, "1")
+        }).stream().map(r -> r.get("person.id")).collect(Collectors.toSet());
+
+        // 从职级职等中共查询职级发生变化的数据
+        Set<Object> jobPersonSet = QueryServiceHelper.query(BillTypeConstants.HRPI_EMPJOBREL, "person.id", new QFilter[]{
+                new QFilter("startdate", QCP.large_equals, beginDate),
+                new QFilter("startdate", QCP.less_equals, endDate),
+                new QFilter("businessstatus", QCP.less_equals, "1"),
+                new QFilter("iscurrentversion", QCP.less_equals, "1")
+        }).stream().map(r -> r.get("person.id")).collect(Collectors.toSet());
+
+        // 合并去重,得到日期范围内变动了岗位或者职级的人员信息
+        positionPersonSet.addAll(jobPersonSet);
+
+        List<List<Object>> partition = Lists.partition(new ArrayList<>(positionPersonSet), 100);
+        int totalCount = 0 ;
+        int successCount = 0;
+        for(List<Object> patchList : partition){
+
+            try{
+                List<DynamicObject> taskCol = new ArrayList<>();
+                // 根据人员生成额度信息
+                patchList.stream().forEach(r -> taskCol.add(generalPersonReimTask(r,beginDate,endDate)));
+
+                // 移除人员、年、月已存在的数据;移除岗位没有发生变化的数据
+                removeErrorData(taskCol);
+
+                Object[] successBillIds = SaveServiceHelper.save(taskCol.toArray(new DynamicObject[0]));
+                List<Object> successBillIdList = Arrays.stream(successBillIds).filter(r -> r instanceof DynamicObject).map(r -> ((DynamicObject) r).getPkValue()).collect(Collectors.toList());
+//                OperationResult operationResult = OperationServiceHelper.executeOperate(PersonReimSyncTaskConstant.OPERATE.SYNCPERSONREIM,
+//                        PersonReimSyncTaskConstant.ENTITYID, successBillIdList.toArray(), OperateOption.create());
+//                totalCount+=successBillIds.length;
+//                successCount+=operationResult.getSuccessPkIds().size();
+
+            }catch (Exception e){
+                // 失败的怎么处理
+                logger.error("分批执行创建人员额度同步任务异常",e);
+            }
+        }
+        if(totalCount != successCount){
+            throw new KDBizException(String.format("总共执行%s条数据,存在%s条失败,请在人员同步任务中查看失败信息。",totalCount,totalCount-successCount));
+        }
+
+    }
+
+    protected void removeErrorData(List<DynamicObject> taskCol){
+        // 移除空数据
+        taskCol.removeIf(r -> Objects.isNull(r));
+
+        // 移除岗位、职级没有发生变化的数据(只有公司发生变动不处理)
+//        taskCol.removeIf(r -> r.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR_PRE) != null && r.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR).getPkValue()
+//                .equals(r.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR_PRE).getPkValue())
+//                && r.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR_PRE) != null && r.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR).getPkValue()
+//                .equals(r.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR_PRE).getPkValue()));
+
+        // 移除已存在的数据
+        Iterator<DynamicObject> it = taskCol.iterator();
+        while (it.hasNext()){
+            DynamicObject taskInfo = it.next();
+            QFilter[] filterList = new QFilter[]{
+                    new QFilter(PersonReimSyncTaskConstant.KEY_NCKD_USER,QCP.equals,taskInfo.getDynamicObject(PersonReimSyncTaskConstant.KEY_NCKD_USER).getPkValue()),
+                    new QFilter(PersonReimSyncTaskConstant.KEY_NCKD_YEAR,QCP.equals,taskInfo.getInt(PersonReimSyncTaskConstant.KEY_NCKD_YEAR)),
+                    new QFilter(PersonReimSyncTaskConstant.KEY_NCKD_MONTH,QCP.equals,taskInfo.getInt(PersonReimSyncTaskConstant.KEY_NCKD_MONTH))
+            };
+
+            if(ORM.create().exists(PersonReimSyncTaskConstant.ENTITYID,filterList)){
+                it.remove();
+            }
+        }
+    }
+
+
+
+    protected DynamicObject generalPersonReimTask(Object personId,Date beginDate , Date endDate){
+        Date now = new Date();
+        DynamicObject taskInfo = BusinessDataServiceHelper.newDynamicObject(PersonReimSyncTaskConstant.ENTITYID);
+        // 设置人员
+        DynamicObject hrPerson = QueryServiceHelper.queryOne(BillTypeConstants.HRPI_PERSON, "number", new QFilter("id", QCP.equals, personId).toArray());
+        DynamicObject userInfo = BusinessDataServiceHelper.loadSingleFromCache("bos_user", new QFilter("number", QCP.equals, hrPerson.getString("number")).toArray());
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_USER,userInfo);
+        // 设置人员最新的岗位、公司信息
+        setNewPosition(taskInfo,personId,beginDate,endDate);
+        // 设置人员最新的职级信息
+        setNewJob(taskInfo,personId,beginDate,endDate);
+        // 设置人员上个月之前的最新岗位、公司信息
+        setOldPosition(taskInfo,personId,beginDate,endDate);
+        // 设置人员上个月之前的最新的职级信息
+        setOldJob(taskInfo,personId,beginDate,endDate);
+
+        // 其他字段初始化赋值
+        int year = CommonUtils.getDateFeild(now, Calendar.YEAR);
+        int month = CommonUtils.getDateFeild(now, Calendar.MONTH);
+
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_YEAR,year);
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_MONTH,month+1);
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_ENABLE,"1");
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_STATUS,"C");
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_SYNCSTATUS, SyncStatusEnum.UNSYNC.getValue());
+        // 返回对象
+        return taskInfo;
+    }
+
+    protected void setNewPosition(DynamicObject taskInfo,Object personId,Date beginDate , Date endDate){
+        // 设置人员最新的岗位、公司信息
+        DynamicObject potisionInfo = QueryServiceHelper.queryOne(BillTypeConstants.HRPI_EMPPOSORGREL, "position.id,company.number", new QFilter[]{
+                new QFilter("businessstatus", QCP.less_equals, "1"),
+                new QFilter("iscurrentversion", QCP.less_equals, "1"),
+                new QFilter("person.id", QCP.equals, personId),
+                new QFilter("isprimary", QCP.less_equals, "1")
+        });
+        if(potisionInfo == null){
+            return ;
+        }
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR,potisionInfo.get("position.id"));
+        DynamicObject orgInfo = BusinessDataServiceHelper
+                .loadSingleFromCache("bos_org", new QFilter("number", QCP.equals, potisionInfo.getString("company.number")).toArray());
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_ORG,orgInfo);
+    }
+
+    protected void setNewJob(DynamicObject taskInfo,Object personId,Date beginDate , Date endDate){
+        // 设置人员最新的岗位、公司信息
+        DynamicObject jobInfo = QueryServiceHelper.queryOne(BillTypeConstants.HRPI_EMPJOBREL, "joblevel.id", new QFilter[]{
+                new QFilter("businessstatus", QCP.less_equals, "1"),
+                new QFilter("iscurrentversion", QCP.less_equals, "1"),
+                new QFilter("person.id", QCP.less_equals, personId)
+        });
+        if(jobInfo == null){
+            return ;
+        }
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR,jobInfo.get("joblevel.id"));
+    }
+    protected void setOldPosition(DynamicObject taskInfo,Object personId,Date beginDate , Date endDate){
+        // 开始日期之前,最新的岗位
+        DynamicObjectCollection positionCol = QueryServiceHelper.query(BillTypeConstants.HRPI_EMPPOSORGREL, "position.id,company.number", new QFilter[]{
+                new QFilter("enddate", QCP.less_than, beginDate),
+                new QFilter("iscurrentversion", QCP.less_equals, "1"),
+                new QFilter("person.id", QCP.equals, personId),
+                new QFilter("isprimary", QCP.less_equals, "1")
+        }, "enddate desc", 1);
+        if(CollectionUtils.isEmpty(positionCol)){
+            return ;
+        }
+        DynamicObject potisionInfo = positionCol.get(0);
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_POSITIONHR_PRE,potisionInfo.get("position.id"));
+        DynamicObject orgInfo = BusinessDataServiceHelper
+                .loadSingleFromCache("bos_org", new QFilter("number", QCP.equals, potisionInfo.getString("company.number")).toArray());
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_ORG_PRE,orgInfo);
+    }
+
+
+    protected void setOldJob(DynamicObject taskInfo,Object personId,Date beginDate , Date endDate){
+        // 设置人员最新的岗位、公司信息
+        DynamicObjectCollection jobCol = QueryServiceHelper.query(BillTypeConstants.HRPI_EMPJOBREL, "joblevel.id", new QFilter[]{
+                new QFilter("startdate", QCP.less_than, beginDate),
+                new QFilter("iscurrentversion", QCP.less_equals, "1"),
+                new QFilter("person.id", QCP.less_equals, personId)
+        }, "enddate desc", 1);
+        if(CollectionUtils.isEmpty(jobCol)){
+            return ;
+        }
+        taskInfo.set(PersonReimSyncTaskConstant.KEY_NCKD_JOBLEVELHR_PRE,jobCol.get(0).get("joblevel.id"));
+    }
+}