Browse Source

feat(hr): 新增三定计划相关功能及常量定义

- 在 FormConstant 中增加撤销、保存、修改等操作常量
- 注释掉 QuitApplyFormPlugin 中的 preOpenForm 方法重写
- 移除 RetireApplyBaseBillListPlugin 中的自定义参数传递逻辑
- 新增 AppflgConstant 常量类用于 hr 云 init 应用标识
- 新增 SanDingConstant 类定义三定管理相关常量
- 新增 SanDingPlanEntryStatus 枚举定义三定计划单据体状态
- 新增 SanDingPlanFormPlugin 实现三定计划表单初始化与操作控制
- 新增 SanDingPlanListPlugin 实现三定计划列表操作后刷新功能
- 新增 SendTaskOpPlugin 实现三定任务发起操作逻辑
jtd 2 days ago
parent
commit
02e874dd00
18 changed files with 905 additions and 7 deletions
  1. 8 0
      code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/constant/FormConstant.java
  2. 2 2
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/htm/plugin/form/quitapply/QuitApplyFormPlugin.java
  3. 0 5
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/htm/plugin/form/quitapply/RetireApplyBaseBillListPlugin.java
  4. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/business/.gitkeep
  5. 22 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/common/AppflgConstant.java
  6. 83 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/common/SanDingConstant.java
  7. 30 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/common/SanDingPlanEntryStatus.java
  8. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/mservice/.gitkeep
  9. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/form/.gitkeep
  10. 212 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/form/SanDingPlanFormPlugin.java
  11. 26 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/form/SanDingPlanListPlugin.java
  12. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/operate/.gitkeep
  13. 444 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/operate/SendTaskOpPlugin.java
  14. 78 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/operate/UpdateSanDingPlanOpPlugin.java
  15. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/other/.gitkeep
  16. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/report/.gitkeep
  17. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/workflow/.gitkeep
  18. 0 0
      code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/webapi/.gitkeep

+ 8 - 0
code/base/nckd-jxccl-base-common/src/main/java/nckd/jxccl/base/common/constant/FormConstant.java

@@ -133,6 +133,8 @@ public class FormConstant {
     public static final String BTN_OK_OP = "btnok";
     /** 提交操作 */
     public static final String SUBMIT_OP = "submit";
+    /** 撤销 */
+    public static final String UNSUBMIT_OP = "unsubmit";
     /** 启用操作 */
     public static final String ENABLE_OP = "enable";
     /** 审核操作 */
@@ -147,6 +149,8 @@ public class FormConstant {
     public static final String AFFIRM_OP = "affirm";
     /** 保存按钮标识 */
     public static final String SAVE_OP = "save";
+    /** 保存按钮标识 */
+    public static final String BAR_SAVE_KEY = "bar_save";
     /** 保存并新增按钮标识 */
     public static final String SAVE_AND_NEW_OP = "saveandnew";
     /** 退出按钮 */
@@ -161,6 +165,10 @@ public class FormConstant {
     public static final String DELETEENTRY_OP = "deleteentry";
     /** 确认操作*/
     public static final String CONFIRM_OP = "confirm";
+    /** 修改操作 */
+    public static final String MODIFY_OP = "modify";
+    /** 修改按钮标识 */
+    public static final String BAR_MODIFY_KEY = "bar_modify";
 
 
     //====================================== 标品页面控件 ======================================

+ 2 - 2
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/htm/plugin/form/quitapply/QuitApplyFormPlugin.java

@@ -31,7 +31,7 @@ public class QuitApplyFormPlugin extends AbstractFormPlugin {
     /** 培养协议合同类型编码 */
     private static final String CONTRACTTYPE_NUMBER = "JT01";
 
-    @Override
+    /*@Override
     public void preOpenForm(PreOpenFormEventArgs e) {
         super.preOpenForm(e);
 
@@ -42,7 +42,7 @@ public class QuitApplyFormPlugin extends AbstractFormPlugin {
         if (!QuitApplyConstant.NCKD_RETIREAPPLY_ENTITY.equals(formId) && customParams == null || !customParams.containsKey("isFromRetire")) {
             e.setCancel(true);
         }
-    }
+    }*/
 
     @Override
     public void propertyChanged(PropertyChangedArgs e) {

+ 0 - 5
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/htm/plugin/form/quitapply/RetireApplyBaseBillListPlugin.java

@@ -24,16 +24,12 @@ public class RetireApplyBaseBillListPlugin extends AbstractListPlugin {
         super.afterDoOperation(afterDoOperationEventArgs);
 
         if (afterDoOperationEventArgs.getOperationResult() != null && afterDoOperationEventArgs.getOperationResult().isSuccess()) {
-            // 自定义参数
-            Map customs = new HashMap();
-            customs.put("isFromRetire", true);
             // 新增退休待办申请
             if (StringUtils.equals(QuitApplyConstant.RETIRE_APPLE_OP,afterDoOperationEventArgs.getOperateKey())) {
                 BillShowParameter showParameter = new BillShowParameter();
                 showParameter.setFormId(QuitApplyConstant.NCKD_RETIREAPPLY_ENTITY);
                 showParameter.setCaption("新增代退休申请");
                 showParameter.getOpenStyle().setShowType(ShowType.MainNewTabPage);
-                showParameter.setCustomParam("isFromRetire", true);
                 getView().showForm(showParameter);
             }
         }
@@ -50,7 +46,6 @@ public class RetireApplyBaseBillListPlugin extends AbstractListPlugin {
         showParameter.setFormId(QuitApplyConstant.NCKD_RETIREAPPLY_ENTITY);
         showParameter.setPkId(getFocusRowPkId());
         showParameter.getOpenStyle().setShowType(ShowType.MainNewTabPage);
-        showParameter.setCustomParam("isFromRetire", true);
         getView().showForm(showParameter);
     }
 }

+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/business/.gitkeep


+ 22 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/common/AppflgConstant.java

@@ -0,0 +1,22 @@
+/**
+ * This is a kingdee cosmic template project that is automatically generated by the Kingdee cosmic development assistant plugin. 
+ * If there are any issues during the use process, you can provide feedback to the kingdee developer community website.
+ * Website: https://developer.kingdee.com/developer?productLineId=29
+ * Author: liebin.zheng
+ * Generate Date: 2025-05-26 16:28:10
+ */
+package nckd.jxccl.hr.sdm.common;
+
+/**
+ * hr云init应用-通用常量类<br>
+ * 代码中不能存在硬编码敏感信息,如账号、密码、http外链、ftp外链、邮箱等。<br>
+ * 标识或缓存的常量,需以"KEY_"、"FID_"、"ENTRY_"或"SUBENTRY_"作为变量的前缀。<br>
+ *
+ * @author nckd
+ * @date 2025-05-26 16:28:10
+ */
+public class AppflgConstant {
+	
+	public static final String KEY_APP_NAME = "hr-init";
+
+}

+ 83 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/common/SanDingConstant.java

@@ -0,0 +1,83 @@
+package nckd.jxccl.hr.sdm.common;
+
+import nckd.jxccl.base.common.constant.FormConstant;
+
+/**
+ * 三定管理常量
+ * @author: jtd
+ * @date: 2025/12/2 20:36
+ */
+public class SanDingConstant extends FormConstant {
+
+    /** 公司负责人实体标识 */
+    public static final String NCKD_COMPANYMANAGER_ENTITY = "nckd_companymanager";
+    /** 三定任务实体标识 */
+    public static final String NCKD_SANDINGTASK_ENTITY = "nckd_sandingtask";
+    /** 三定批次实体标识 */
+    public static final String NCKD_SANDINGTIME_ENTITY = "nckd_sandingtime";
+    /** 三定计划实体标识 */
+    public static final String NCKD_SANDINGPLAN_ENTITY = "nckd_sandingplan";
+    /** 三定计划分录实体标识 */
+    public static final String NCKD_SANDINGPLAN_ENTRY_ENTITY = "nckd_sandingplan_entry";
+
+    /** 发送任务操作 */
+    public static final String SENDTASK_OP = "sendtask";
+    /** 发送任务标识 */
+    public static final String NCKD_SENDTASK_KEY = "nckd_sendtask";
+
+    /** 单位 */
+    public static final String NCKD_COMPANY_KEY = "nckd_company";
+    /** 负责人 */
+    public static final String NCKD_MANAGER_KEY = "nckd_manager";
+    /** 用工关系状态 */
+    public static final String NCKD_LABORRELSTATUS_KEY = "nckd_laborrelstatus";
+    /** 三定计划 */
+    public static final String NCKD_SANDINGPLAN_KEY = "nckd_sandingplan";
+    /** 三定计划分录 */
+    public static final String NCKD_SDP_ENTRY_KEY = "nckd_sdp_entry";
+    /** 实际占编人数 */
+    public static final String NCKD_ACTUALSTAFFCOUNT_KEY = "nckd_actualstaffcount";
+    /** 前一年流失人数(系统数)  */
+    public static final String NCKD_Y1OUT_SYS_KEY = "nckd_y1out_sys";
+    /** 前两年流失人数(系统数)  */
+    public static final String NCKD_Y2OUT_SYS_KEY = "nckd_y2out_sys";
+    /** 前三年流失人数(系统数)  */
+    public static final String NCKD_Y3OUT_SYS_KEY = "nckd_y3out_sys";
+    /** 前一年流失人数(确认数)  */
+    public static final String NCKD_Y1OUT_CONF_KEY = "nckd_y1out_conf";
+    /** 前两年流失人数(确认数)  */
+    public static final String NCKD_Y2OUT_CONF_KEY = "nckd_y2out_conf";
+    /** 前三年流失人数(确认数)  */
+    public static final String NCKD_Y3OUT_CONF_KEY = "nckd_y3out_conf";
+    /** 年度 */
+    public static final String NCKD_SANDINGYEAR_KEY = "nckd_sandingyear";
+    /** 年度(字符串) */
+    public static final String NCKD_SANDINGYEAR_TEXT_KEY = "nckd_sandingyear_text";
+    /** 适用批次人力资源需求批次 */
+    public static final String NCKD_SANDINGTIME_KEY = "nckd_sandingtime";
+    /** 状态 */
+    public static final String NCKD_STATUS_KEY = "nckd_status";
+    /** 适用生效日期 */
+    public static final String NCKD_STARTDATE_KEY = "nckd_startdate";
+    /** 是否发送 */
+    public static final String NCKD_ISSEND_KEY = "nckd_issend";
+    /** 是否完成 */
+    public static final String NCKD_ISCOMPLATED_KEY = "nckd_iscomplated";
+    /** 上一批次适用生效日期 */
+    public static final String NCKD_STARTDATE_LAST_KEY = "nckd_startdate_last";
+    /** 定员数 */
+    public static final String NCKD_AUTHORIZEDSTRENGTH_KEY = "nckd_authorizedstrength";
+    /** 缺编人数 */
+    public static final String NCKD_STAFFINGSHORTFALL_KEY = "nckd_staffingshortfall";
+
+    /** 行政组织.ID */
+    public static final String HAOS_ADMINORGHR_ID = "haos_adminorghr.id";
+    /** 行政组织.业务ID */
+    public static final String HAOS_ADMINORGHR_BOID = "haos_adminorghr.boid";
+    /** 行政组织.关联历史版本 */
+    public static final String HAOS_ADMINORGHR_SOURCEVID = "haos_adminorghr.sourcevid";
+    /** HR行政组织.所属公司.业务ID */
+    public static final String HAOS_ADMINORGHR_BELONGCOMPANY_BOID = "haos_adminorghr.belongcompany.boid";
+
+
+}

+ 30 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/common/SanDingPlanEntryStatus.java

@@ -0,0 +1,30 @@
+package nckd.jxccl.hr.sdm.common;
+
+/**
+ * 三定计划单据体状态枚举
+ * @author: jtd
+ * @date: 2025/12/11 11:47
+ */
+public enum SanDingPlanEntryStatus {
+
+    UNSEND("A", "未发起"),
+    UNSUBMIT("B", "未提交"),
+    SUBMIT("C", "已提交"),
+    COMPLETED("D", "已完成");
+
+    private String code;
+    private String name;
+
+    private SanDingPlanEntryStatus(String code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public String getCode() {
+        return this.code;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+}

+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/mservice/.gitkeep


+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/form/.gitkeep


+ 212 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/form/SanDingPlanFormPlugin.java

@@ -0,0 +1,212 @@
+package nckd.jxccl.hr.sdm.plugin.form;
+
+import kd.bos.bill.OperationStatus;
+import kd.bos.common.enums.EnableEnum;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.datamodel.events.ChangeData;
+import kd.bos.entity.datamodel.events.PropertyChangedArgs;
+import kd.bos.form.events.AfterDoOperationEventArgs;
+import kd.bos.form.field.DateEdit;
+import kd.bos.form.operate.FormOperate;
+import kd.bos.form.plugin.AbstractFormPlugin;
+import kd.bos.mvc.base.BaseModel;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.common.util.HRDateTimeUtils;
+import kd.hr.hbp.common.util.HRObjectUtils;
+import kd.hr.hbp.common.util.HRStringUtils;
+import nckd.jxccl.base.common.utils.QueryFieldBuilder;
+import nckd.jxccl.base.orm.helper.QFilterCommonHelper;
+import nckd.jxccl.hr.sdm.common.SanDingConstant;
+import nckd.jxccl.hr.sdm.common.SanDingPlanEntryStatus;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 三定计划表单插件
+ * @entity: nckd_sandingplan
+ * @author: jtd
+ * @date: 2025/12/2 18:28
+ */
+public class SanDingPlanFormPlugin extends AbstractFormPlugin {
+
+    @Override
+    public void afterCreateNewData(EventObject e) {
+        super.afterCreateNewData(e);
+
+        DynamicObject dyo = ((BaseModel) e.getSource()).getDataEntity();
+        // 初始化单头数据
+        initBillData(dyo.getDate(SanDingConstant.NCKD_SANDINGYEAR_KEY));
+        // 初始化单据体数据
+        initEntryData();
+    }
+
+    private void initBillData(Date sanDingYear) {
+        if (HRObjectUtils.isEmpty(sanDingYear)) {
+            return;
+        }
+        // 获取年
+        int year = HRDateTimeUtils.getYear(sanDingYear);
+        // 查询 适用生效日期、适用批次人力资源需求批次.编码
+        String selectProperties = QueryFieldBuilder.create()
+                .add(SanDingConstant.NCKD_STARTDATE_KEY)
+                .add(SanDingConstant.NCKD_SANDINGTIME_KEY, SanDingConstant.NUMBER_KEY)
+                .buildSelect();
+        QFilter qFilter = new QFilter(SanDingConstant.NCKD_SANDINGYEAR_TEXT_KEY, QCP.equals, String.valueOf(year))
+                .and(SanDingConstant.NCKD_ISCOMPLATED_KEY, QCP.equals, EnableEnum.YES.getCode());
+        String orderBys = QueryFieldBuilder.create().addDesc(SanDingConstant.NCKD_SANDINGTIME_KEY).buildOrder();
+        // 判断 是否存在三定计划数据
+        DynamicObject[] sanDingPlanDyos = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGPLAN_ENTITY).load(selectProperties, new QFilter[]{qFilter}, orderBys);
+        HRBaseServiceHelper sanDingTimeServiceHelper = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGTIME_ENTITY);
+        if (sanDingPlanDyos == null || sanDingPlanDyos.length == 0) {
+            // 设置 适用批次人力资源需求批次
+            DynamicObject sanDingTimeDyo = sanDingTimeServiceHelper.loadDynamicObject(new QFilter(SanDingConstant.NUMBER_KEY, QCP.equals, "1"));
+            if (!HRObjectUtils.isEmpty(sanDingTimeDyo)) {
+                getModel().setValue(SanDingConstant.NCKD_SANDINGTIME_KEY, sanDingTimeDyo);
+            }
+            // 设置 上一批次适用生效日期
+            getModel().setValue(SanDingConstant.NCKD_STARTDATE_LAST_KEY, null);
+        } else {
+            // 获取 年度最后一个批次的计划数据
+            DynamicObject sanDingPlanDyo = sanDingPlanDyos[0];
+            // 获取 批次编码
+            int sanDingTimeNumber = sanDingPlanDyo.getInt(String.join(".", SanDingConstant.NCKD_SANDINGTIME_KEY, SanDingConstant.NUMBER_KEY));
+            String nextSanDingTimeNumber = String.valueOf(sanDingTimeNumber + 1);
+            // 获取 下一个批次的对象
+            DynamicObject sanDingTimeDyo = sanDingTimeServiceHelper.loadDynamicObject(new QFilter(SanDingConstant.NUMBER_KEY, QCP.equals, nextSanDingTimeNumber));
+            // 如果存在值,则设置适用批次人力资源需求批次
+            if (!HRObjectUtils.isEmpty(sanDingPlanDyo)) {
+                getModel().setValue(SanDingConstant.NCKD_SANDINGTIME_KEY, sanDingTimeDyo);
+            } else {
+                getView().showTipNotification(HRStringUtils.format("请先维护编码[{}]的三定批次基础数据", nextSanDingTimeNumber));
+            }
+            // 设置 上一批次适用生效日期
+            getModel().setValue(SanDingConstant.NCKD_STARTDATE_LAST_KEY, sanDingPlanDyo.getDate(SanDingConstant.NCKD_STARTDATE_KEY));
+        }
+    }
+
+    private void initEntryData() {
+        // 单位Key
+        String companyIdKey = String.join(".", SanDingConstant.NCKD_COMPANY_KEY, SanDingConstant.ID_KEY);
+        // 负责人Key
+        String managerIdKey = String.join(".", SanDingConstant.NCKD_MANAGER_KEY, SanDingConstant.ID_KEY);
+        // 查询公司负责人
+        String selectFields = QueryFieldBuilder.create()
+                .add(companyIdKey)
+                .add(managerIdKey)
+                .buildSelect();
+        // 排序 物理层级、排序号
+        String orderBys = QueryFieldBuilder.create()
+                .addAsc(SanDingConstant.NCKD_COMPANY_KEY, SanDingConstant.LEVEL_KEY)
+                .addAsc(SanDingConstant.NCKD_COMPANY_KEY, SanDingConstant.INDEX_KEY)
+                .buildOrder();
+        DynamicObject[] companyManagerDyos = HRBaseServiceHelper.create(SanDingConstant.NCKD_COMPANYMANAGER_ENTITY).queryOriginalArray(selectFields, null, orderBys);
+        // 处理数据
+        Map<Long, Long> companyManagerMap = new LinkedHashMap<>();
+        Set<Long> companyIds = new HashSet<Long>();
+        Set<Long> managerIds = new HashSet<Long>();
+        for (int i = 0; i < companyManagerDyos.length; i++) {
+            DynamicObject companyManagerDyo = companyManagerDyos[i];
+            Long companyId = companyManagerDyo.getLong(companyIdKey);
+            Long managerId = companyManagerDyo.getLong(managerIdKey);
+            companyManagerMap.put(companyId, managerId);
+            companyIds.add(companyId);
+            managerIds.add(managerId);
+        }
+        // 获取单位数据
+        DynamicObject[] companyDyos = HRBaseServiceHelper.create(SanDingConstant.ADMINORGHR_ENTITYID).load(new QFilter[]{QFilterCommonHelper.getIdInFilter(companyIds)});
+        Map<Long, DynamicObject> companyMap = Arrays.stream(companyDyos).collect(Collectors.toMap(companyDyo -> companyDyo.getLong(SanDingConstant.ID_KEY), Function.identity(), (oldValue, newValue) -> newValue));
+        // 获取负责任人数据
+        DynamicObject[] managerDyos = HRBaseServiceHelper.create(SanDingConstant.HRPI_EMPLOYEE).load(new QFilter[]{QFilterCommonHelper.getIdInFilter(managerIds)});
+        Map<Long, DynamicObject> managerMap = Arrays.stream(managerDyos).collect(Collectors.toMap(managerDyo -> managerDyo.getLong(SanDingConstant.ID_KEY), Function.identity(), (oldValue, newValue) -> newValue));
+        // 清空单据体数据
+        DynamicObjectCollection entryEntityColl = getModel().getEntryEntity(SanDingConstant.NCKD_ENTRYENTITY);
+        entryEntityColl.clear();
+        // 写入单据体
+        for (DynamicObject companyManagerDyo : companyManagerDyos) {
+            DynamicObject entryDyo = entryEntityColl.addNew();
+            // 设置 单位
+            entryDyo.set(SanDingConstant.NCKD_COMPANY_KEY, companyMap.get(companyManagerDyo.getLong(companyIdKey)));
+            // 设置 负责人
+            entryDyo.set(SanDingConstant.NCKD_MANAGER_KEY, managerMap.get(companyManagerDyo.getLong(managerIdKey)));
+            // 设置 状态
+            entryDyo.set(SanDingConstant.NCKD_STATUS_KEY, SanDingPlanEntryStatus.UNSEND.getCode());// 默认未发起
+        }
+        // 更新单据体数据缓存和界面
+        getModel().updateEntryCache(entryEntityColl);
+        getView().updateView(SanDingConstant.NCKD_ENTRYENTITY);
+    }
+
+    @Override
+    public void afterBindData(EventObject e) {
+        super.afterBindData(e);
+
+        OperationStatus status = getView().getFormShowParameter().getStatus();
+        if (status == OperationStatus.ADDNEW || status == OperationStatus.EDIT) {
+            DynamicObject dyo = getModel().getDataEntity();
+            // 设置 适用生效日期最小值
+            Date startDateLast = dyo.getDate(SanDingConstant.NCKD_STARTDATE_LAST_KEY);
+            setStartDateMinDate(startDateLast);
+        }
+        DynamicObject dyo = getModel().getDataEntity();
+        boolean isSend = dyo.getBoolean(SanDingConstant.NCKD_ISSEND_KEY);
+        boolean isComplated = dyo.getBoolean(SanDingConstant.NCKD_ISCOMPLATED_KEY);
+        if (isSend || isComplated) {
+            getView().setVisible(Boolean.FALSE, SanDingConstant.BAR_MODIFY_KEY, SanDingConstant.NCKD_SENDTASK_KEY);
+        }
+    }
+
+    private void setStartDateMinDate(Date startDateLast) {
+        DateEdit startDateControl = getControl(SanDingConstant.NCKD_STARTDATE_KEY);
+        if (!HRObjectUtils.isEmpty(startDateLast)) {
+            startDateControl.setMinDate(HRDateTimeUtils.addDay(startDateLast, 1));
+        } else {
+            // 无法清除最小值,只能设置一个最小值
+            startDateControl.setMinDate(new Date(0));
+        }
+    }
+
+    @Override
+    public void propertyChanged(PropertyChangedArgs e) {
+        super.propertyChanged(e);
+
+        String fieldKey = e.getProperty().getName();
+        if (SanDingConstant.NCKD_SANDINGYEAR_KEY.equals(fieldKey)) {
+            ChangeData[] changeSet = e.getChangeSet();
+            Date sanDingYear = (Date) changeSet[0].getNewValue();
+            initBillData(sanDingYear);
+            // 清空 适用生效日期
+            getModel().setValue(SanDingConstant.NCKD_STARTDATE_KEY, null);
+            // 设置 适用生效日期最小值
+            setStartDateMinDate((Date) getModel().getValue(SanDingConstant.NCKD_STARTDATE_LAST_KEY));
+        }
+    }
+
+    @Override
+    public void afterDoOperation(AfterDoOperationEventArgs afterDoOperationEventArgs) {
+        super.afterDoOperation(afterDoOperationEventArgs);
+
+        if (afterDoOperationEventArgs.getOperationResult() != null && afterDoOperationEventArgs.getOperationResult().isSuccess()) {
+            FormOperate operate = (FormOperate) afterDoOperationEventArgs.getSource();
+            String operateKey = operate.getOperateKey();
+            if (HRStringUtils.equals(operateKey, SanDingConstant.SAVE_OP)) {
+                // 隐藏保存按钮
+                getView().setVisible(Boolean.FALSE, SanDingConstant.BAR_SAVE_KEY);
+                // 设置页面状态为查看
+                getView().setStatus(OperationStatus.VIEW);
+                // 刷新
+                getView().invokeOperation(SanDingConstant.REFRESH_OP);
+            }
+        }
+    }
+}

+ 26 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/form/SanDingPlanListPlugin.java

@@ -0,0 +1,26 @@
+package nckd.jxccl.hr.sdm.plugin.form;
+
+import kd.bos.form.events.AfterDoOperationEventArgs;
+import kd.bos.list.plugin.AbstractListPlugin;
+import kd.hr.hbp.common.util.HRStringUtils;
+import nckd.jxccl.hr.sdm.common.SanDingConstant;
+
+/**
+ * 三定计划列表插件
+ * @entity: nckd_sandingplan
+ * @author: jtd
+ * @date: 2025/12/12 16:52
+ */
+public class SanDingPlanListPlugin extends AbstractListPlugin {
+
+    @Override
+    public void afterDoOperation(AfterDoOperationEventArgs afterDoOperationEventArgs) {
+        super.afterDoOperation(afterDoOperationEventArgs);
+
+        String operateKey = afterDoOperationEventArgs.getOperateKey();
+        if (HRStringUtils.equals(operateKey, SanDingConstant.SENDTASK_OP)) {
+            // 刷新
+            getView().invokeOperation(SanDingConstant.REFRESH_OP);
+        }
+    }
+}

+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/operate/.gitkeep


+ 444 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/operate/SendTaskOpPlugin.java

@@ -0,0 +1,444 @@
+package nckd.jxccl.hr.sdm.plugin.operate;
+
+import kd.bos.coderule.api.CodeRuleInfo;
+import kd.bos.coderule.opplugin.util.OrgUtil;
+import kd.bos.common.enums.EnableEnum;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.dataentity.entity.LocaleString;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.QueryEntityType;
+import kd.bos.entity.operate.result.IOperateInfo;
+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.BeginOperationTransactionArgs;
+import kd.bos.entity.plugin.args.EndOperationTransactionArgs;
+import kd.bos.exception.ErrorCode;
+import kd.bos.exception.KDBizException;
+import kd.bos.message.util.MessageUtils;
+import kd.bos.orm.ORM;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.coderule.CodeRuleServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.bos.servicehelper.user.UserServiceHelper;
+import kd.bos.url.UrlService;
+import kd.bos.workflow.engine.msg.info.MessageInfo;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.business.servicehelper.HRQueryEntityHelper;
+import kd.hr.hbp.common.util.HRDateTimeUtils;
+import kd.hr.hbp.common.util.HRObjectUtils;
+import kd.sdk.hr.hrpi.business.helper.HRPIEmployeeServiceHelper;
+import nckd.jxccl.base.common.utils.QueryFieldBuilder;
+import nckd.jxccl.base.org.helper.OrgHelper;
+import nckd.jxccl.base.orm.helper.QFilterCommonHelper;
+import nckd.jxccl.hr.sdm.common.SanDingConstant;
+import nckd.jxccl.hr.sdm.common.SanDingPlanEntryStatus;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 发起任务OP插件
+ * @entity: nckd_sandingplan
+ * @operat: sendtask
+ * @author: jtd
+ * @date: 2025/12/3 15:01
+ */
+public class SendTaskOpPlugin extends AbstractOperationServicePlugIn {
+
+    /** 特殊借调变动操作编码 */
+    private static final String TSJD_CHGACTION_NUMBER = "JTCC_1001";
+    /** 跨公司调动 */
+    private static final String KGSDD_CHGACTION_NUMBER = "101060_S";
+    /** 获取岗位信息查询编码 */
+    private static final String NCKD_SDM_GETPOSITIONQUERY = "nckd_sdm_getpositionquery";
+    /** orm */
+    private static final ORM ORM_IMPL = ORM.create();
+    /** key->三定计划ID value->三定任务ID */
+    private static final Map<Long, Long> SANDING_TASK_MAP = new HashMap<Long, Long>();
+
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        super.onPreparePropertys(e);
+
+        e.getFieldKeys().addAll(billEntityType.getAllFields().keySet());
+    }
+
+    @Override
+    public void beginOperationTransaction(BeginOperationTransactionArgs e) {
+        super.beginOperationTransaction(e);
+
+        try {
+
+
+            DynamicObject[] dyos = e.getDataEntities();
+            for (DynamicObject dyo : dyos) {
+                DynamicObjectCollection entryDyoColl = dyo.getDynamicObjectCollection(SanDingConstant.NCKD_ENTRYENTITY);
+                sendTask(dyo, entryDyoColl);
+                // 设置 已发送
+                dyo.set(SanDingConstant.NCKD_ISSEND_KEY, EnableEnum.YES.getCode());
+            }
+            // 保存数据
+            SaveServiceHelper.save(dyos);
+        } catch (Exception ex) {
+            throw new KDBizException(ex, new ErrorCode("sendTaskError", ex.getMessage()));
+        }
+    }
+
+    private static void sendTask(DynamicObject billDyo, DynamicObjectCollection entryDyoColl) throws Exception {
+        // key 是否在职
+        String isHiredKey = String.join(".", SanDingConstant.NCKD_LABORRELSTATUS_KEY, SanDingConstant.IS_HIRED);
+        // Key 岗位BOID
+        String positionBoIdKey = String.join(".", SanDingConstant.POSITION_KEY, SanDingConstant.BOID_KEY);
+        // Key 员工BOID
+        String employeeBoIdKey = String.join(".", SanDingConstant.EMPLOYEE_KEY, SanDingConstant.BOID_KEY);
+        // Key 单位BOID
+        String companyBoIdKey = String.join(".", SanDingConstant.NCKD_COMPANY_KEY, SanDingConstant.BOID_KEY);
+        // 获取 所有单位BOID
+        Set<Long> companyBoIds = entryDyoColl.stream().map(entryDyo -> entryDyo.getLong(companyBoIdKey)).collect(Collectors.toSet());
+        // 获取 所有岗位数据
+        QFilter qFilter = new QFilter(String.join(".", SanDingConstant.ADMINORG, SanDingConstant.BELONGCOMPANY_KEY, SanDingConstant.BOID_KEY),  QCP.in, companyBoIds).and(QFilterCommonHelper.getCurrentVersionFilter());
+        // 查询 BOID、行政组织、行政组织.所属公司
+        String queryFields = QueryFieldBuilder.create()
+                .add(SanDingConstant.BOID_KEY)
+                .add(SanDingConstant.SOURCEVID_KEY)
+                .add(SanDingConstant.HAOS_ADMINORGHR_BOID)
+                .add(SanDingConstant.HAOS_ADMINORGHR_SOURCEVID)
+                .add(SanDingConstant.HAOS_ADMINORGHR_BELONGCOMPANY_BOID)
+                .buildSelect();
+        String orderBys = QueryFieldBuilder.create()
+                .addAsc(SanDingConstant.ADMINORG, SanDingConstant.LEVEL_KEY)
+                .addAsc(SanDingConstant.ADMINORG, SanDingConstant.INDEX_KEY)
+                .addDesc(SanDingConstant.ISLEADER_KEY)
+                .addAsc(SanDingConstant.NUMBER_KEY)
+                .buildOrder();
+        DynamicObjectCollection positionDyoColl = HRQueryEntityHelper.getInstance().getQueryDyoColl((QueryEntityType) EntityMetadataCache.getDataEntityType(NCKD_SDM_GETPOSITIONQUERY), queryFields, new QFilter[]{qFilter}, orderBys);
+        // 获取 所有岗位BOID
+        Set<Long> positionBoIdSet = positionDyoColl.stream().map(positionDyo -> positionDyo.getLong(SanDingConstant.BOID_KEY)).collect(Collectors.toSet());
+        // 处理成 key->公司BOID value->List<Map<String, Long>>
+        Map<Long, List<Map<String, Long>>> companyPositionMap = positionDyoColl.stream()
+            .collect(Collectors.groupingBy(
+                positionDyo -> positionDyo.getLong(SanDingConstant.HAOS_ADMINORGHR_BELONGCOMPANY_BOID),
+                Collectors.mapping(
+                    positionDyo -> {
+                        Map<String, Long> positionOrgMap = new HashMap<>();
+                        positionOrgMap.put("positionBoId", positionDyo.getLong(SanDingConstant.BOID_KEY));
+                        positionOrgMap.put("positionSourceVid", positionDyo.getLong(SanDingConstant.BOID_KEY));
+                        positionOrgMap.put("adminOrgBoId", positionDyo.getLong(SanDingConstant.HAOS_ADMINORGHR_BOID));
+                        positionOrgMap.put("adminOrgSourceVid", positionDyo.getLong(SanDingConstant.HAOS_ADMINORGHR_BOID));
+                        return positionOrgMap;
+                    },
+                    Collectors.toList()
+                )
+            ));
+        // 获取 所有岗位当前在职人数
+        HRBaseServiceHelper empPosOrgRelServiceHelper = HRBaseServiceHelper.create(SanDingConstant.HRPI_EMPPOSORGREL);
+        // 获取当前日期
+        Date nowDate = HRDateTimeUtils.getNowDate();
+        // 过滤 主任职 并且 开始日期<=当前日期 并且 结束日期>=当前日期 并且 用工关系状态.是否在职=1 并且 变动操作.编码<>特殊借调 并且 岗位BOID
+        qFilter = new QFilter(SanDingConstant.IS_PRIMARY, QCP.equals, EnableEnum.YES.getCode())
+                .and(new QFilter(SanDingConstant.STARTDATE, QCP.less_equals, nowDate))
+                .and(new QFilter(SanDingConstant.ENDDATE, QCP.large_equals, nowDate))
+                .and(new QFilter(isHiredKey, QCP.equals, EnableEnum.YES.getCode()))
+                .and(new QFilter(String.join(".", SanDingConstant.CHGACTION, SanDingConstant.NUMBER_KEY), QCP.not_equals, TSJD_CHGACTION_NUMBER))
+                .and(new QFilter(positionBoIdKey, QCP.in, positionBoIdSet));
+        // 排序 行政组织.物理层级、行政组织.排序号、岗位.是否负责人岗位
+        orderBys = QueryFieldBuilder.create()
+                .addAsc(SanDingConstant.ADMINORG, SanDingConstant.LEVEL_KEY)
+                .addAsc(SanDingConstant.ADMINORG, SanDingConstant.INDEX_KEY)
+                .addDesc(SanDingConstant.POSITION_KEY, SanDingConstant.ISLEADER_KEY)
+                .buildOrder();
+        DynamicObject[] empPosOrgRelDyos = empPosOrgRelServiceHelper.queryOriginalArray(positionBoIdKey, new QFilter[]{qFilter}, orderBys);
+        // 处理成 key->岗位BOID value->岗位当前在职人数
+        Map<Long, Integer> positionInStaffEmployeeCountMap = Arrays.stream(empPosOrgRelDyos).map(empPosOrgRelDyo -> empPosOrgRelDyo.getLong(positionBoIdKey)).collect(Collectors.groupingBy(Function.identity(), Collectors.collectingAndThen(Collectors.counting(), Long::intValue)));
+        // 获取上年
+        String y1 = String.valueOf(HRDateTimeUtils.getYear(HRDateTimeUtils.getNowDate())-1);
+        // 获取上上年
+        String y2 = String.valueOf(HRDateTimeUtils.getYear(HRDateTimeUtils.getNowDate())-2);
+        // 获取上上上年
+        String y3 = String.valueOf(HRDateTimeUtils.getYear(HRDateTimeUtils.getNowDate())-3);
+        // 获取上上上上年
+        String y4 = String.valueOf(HRDateTimeUtils.getYear(HRDateTimeUtils.getNowDate())-4);
+        // 过滤 主任职 并且 用工关系状态.是否在职<>1 并且 开始日期>=三年前年初 并且 开始日期<=去年年末 并且 岗位BOID
+        qFilter = new QFilter(SanDingConstant.IS_PRIMARY, QCP.equals, EnableEnum.YES.getCode())
+                .and(new QFilter(isHiredKey, QCP.not_equals, EnableEnum.YES.getCode()))
+                .and(new QFilter(SanDingConstant.STARTDATE, QCP.large_equals, HRDateTimeUtils.parseDate(y3+"-01-01", HRDateTimeUtils.YYYY_MM_DD)))
+                .and(new QFilter(SanDingConstant.STARTDATE, QCP.less_equals, HRDateTimeUtils.parseDate(y1+"-12-31", HRDateTimeUtils.YYYY_MM_DD)))
+                .and(new QFilter(positionBoIdKey, QCP.in, positionBoIdSet));
+        // 查询 岗位ID、开始日期
+        String selectFields = QueryFieldBuilder.create().add(positionBoIdKey).add(SanDingConstant.STARTDATE).buildSelect();
+        // 获取不在职人数
+        empPosOrgRelDyos = empPosOrgRelServiceHelper.queryOriginalArray(selectFields, new QFilter[]{qFilter}, orderBys);
+        // 处理成 key->岗位BOID@开始年 value->岗位不在职人数
+        Map<String, Integer> yOutSysMap = Arrays.stream(empPosOrgRelDyos).map(empPosOrgRelDyo -> empPosOrgRelDyo.getLong(positionBoIdKey) + "@" + HRDateTimeUtils.getYear(empPosOrgRelDyo.getDate(SanDingConstant.STARTDATE))).collect(Collectors.groupingBy(Function.identity(), Collectors.collectingAndThen(Collectors.counting(), Long::intValue)));
+        Map<String, Integer> y1OutSysMap = new HashMap<String, Integer>();
+        Map<String, Integer> y2OutSysMap = new HashMap<String, Integer>();
+        Map<String, Integer> y3OutSysMap = new HashMap<String, Integer>();
+        for (Map.Entry<String, Integer> entry : yOutSysMap.entrySet()) {
+            String[] keys = entry.getKey().split("@");
+            String year = keys[1];
+            if (y1.equals(year)) {
+                y1OutSysMap.put(entry.getKey(), entry.getValue());
+            }
+            if (y2.equals(year)) {
+                y2OutSysMap.put(entry.getKey(), entry.getValue());
+            }
+            if (y3.equals(year)) {
+                y3OutSysMap.put(entry.getKey(), entry.getValue());
+            }
+        }
+        // 过滤 主任职 并且 开始日期>=三年前年初 并且 结束日期<=去年年末 并且 变动操作.编码=跨公司调动 并且 岗位BOID
+        qFilter = new QFilter(SanDingConstant.IS_PRIMARY, QCP.equals, EnableEnum.YES.getCode())
+                .and(new QFilter(SanDingConstant.STARTDATE, QCP.large_equals, HRDateTimeUtils.parseDate(y3+"-01-01", HRDateTimeUtils.YYYY_MM_DD)))
+                .and(new QFilter(SanDingConstant.ENDDATE, QCP.less_equals, HRDateTimeUtils.parseDate(y1+"-12-31", HRDateTimeUtils.YYYY_MM_DD)))
+                .and(new QFilter(String.join(".", SanDingConstant.CHGACTION, SanDingConstant.NUMBER_KEY), QCP.equals, KGSDD_CHGACTION_NUMBER))
+                .and(new QFilter(positionBoIdKey, QCP.in, positionBoIdSet));
+        empPosOrgRelDyos = empPosOrgRelServiceHelper.queryOriginalArray(String.join(".", SanDingConstant.EMPLOYEE_KEY, SanDingConstant.BOID_KEY), new QFilter[]{qFilter}, orderBys);
+        // 获取 员工BOID
+        Set<Long> employeeBoIdSet = new HashSet<Long>();
+        // 处理成 key->员工BOID value->List<跨公司调动任职经历id>
+        Map<Long, List<Long>> employeeBoIdMap = new HashMap<Long, List<Long>>();
+        for (DynamicObject empPosOrgRelDyo : empPosOrgRelDyos) {
+            long employeeBoId = empPosOrgRelDyo.getLong(employeeBoIdKey);
+            if (employeeBoIdMap.containsKey(employeeBoId)) {
+                List<Long> empPosOrgRelIdList = employeeBoIdMap.get(employeeBoId);
+                empPosOrgRelIdList.add(empPosOrgRelDyo.getLong(SanDingConstant.BOID_KEY));
+            } else {
+                List<Long> empPosOrgRelIdList = new ArrayList<Long>();
+                empPosOrgRelIdList.add(empPosOrgRelDyo.getLong(SanDingConstant.BOID_KEY));
+                employeeBoIdMap.put(employeeBoId, empPosOrgRelIdList);
+            }
+            employeeBoIdSet.add(employeeBoId);
+        }
+        // 过滤 主任职 并且 (结束日期>=四年前年末 并且 结束日期 <= 去年年末 或者 开始日期>=三年前年末 并且 结束日期<=去年年末) 并且 员工BOID
+        qFilter = new QFilter(SanDingConstant.IS_PRIMARY, QCP.equals, EnableEnum.YES.getCode())
+                .and(
+                        new QFilter(SanDingConstant.ENDDATE, QCP.large_equals, HRDateTimeUtils.parseDate(y4+"-12-31", HRDateTimeUtils.YYYY_MM_DD))
+                        .and(SanDingConstant.ENDDATE, QCP.less_equals, HRDateTimeUtils.parseDate(y1+"-12-31", HRDateTimeUtils.YYYY_MM_DD))
+                        .or(SanDingConstant.STARTDATE, QCP.large_equals, HRDateTimeUtils.parseDate(y3+"-12-31", HRDateTimeUtils.YYYY_MM_DD))
+                        .and(SanDingConstant.ENDDATE, QCP.less_equals, HRDateTimeUtils.parseDate(y1+"-12-31", HRDateTimeUtils.YYYY_MM_DD))
+                )
+                .and(new QFilter(SanDingConstant.STARTDATE, QCP.large_equals, HRDateTimeUtils.parseDate(y3+"-01-01", HRDateTimeUtils.YYYY_MM_DD)))
+                .and(new QFilter(SanDingConstant.ENDDATE, QCP.less_equals, HRDateTimeUtils.parseDate(y1+"-12-31", HRDateTimeUtils.YYYY_MM_DD)))
+                .and(new QFilter(SanDingConstant.EMPLOYEE_KEY, QCP.in, employeeBoIdSet));
+        selectFields = QueryFieldBuilder.create().add(SanDingConstant.EMPLOYEE_KEY,  SanDingConstant.BOID_KEY).add(SanDingConstant.ID_KEY).add(SanDingConstant.POSITION_KEY, SanDingConstant.BOID_KEY).add(SanDingConstant.ENDDATE).buildSelect();
+        // 获取 员工的所有主任职记录
+        orderBys = QueryFieldBuilder.create().addAsc(SanDingConstant.EMPLOYEE_KEY, SanDingConstant.EMP_NUMBER_KEY).addAsc(SanDingConstant.STARTDATE).buildOrder();
+        empPosOrgRelDyos = empPosOrgRelServiceHelper.queryOriginalArray(selectFields, new QFilter[]{qFilter}, orderBys);
+        // 处理成 key->员工BOID value->List<员工所有主任职记录>
+        Map<Long, List<DynamicObject>> empPosOrgRelDyoListMap = Arrays.stream(empPosOrgRelDyos).collect(Collectors.groupingBy(empPosOrgRelDyo -> empPosOrgRelDyo.getLong(SanDingConstant.EMPLOYEE_KEY)));
+        // 处理成 key->岗位BOID@年 value->List<跨公司调动任职经历id>
+        Map<String, List<Long>> kgsddEmpPosOrgRelIdListMap = new HashMap<String, List<Long>>();
+        // 对于跨公司调动来说,这笔任职属于流入,需要根据人定位到这条任职经历的上一条主任职记录,才是流出的任职记录,流出任职经历的结束时间所在的年才是流出年
+        for (Map.Entry<Long, List<Long>> entry : employeeBoIdMap.entrySet()) {
+            Long employeeBoId = entry.getKey();
+            List<Long> empPosOrgRelIdList = entry.getValue();
+            List<DynamicObject> empPosOrgRelDyoList = empPosOrgRelDyoListMap.get(employeeBoId);
+            for (Long empPosOrgRelId : empPosOrgRelIdList) {
+                for (int i = 0; i < empPosOrgRelDyoList.size(); i++) {
+                    DynamicObject empPosOrgRelDyo = empPosOrgRelDyoList.get(i);
+                    if (empPosOrgRelId == empPosOrgRelDyo.getLong(SanDingConstant.ID_KEY)) {
+                        DynamicObject lastEmpPosOrgRelDyo = empPosOrgRelDyoList.get(i - 1);
+                        Long positionBoId = lastEmpPosOrgRelDyo.getLong(positionBoIdKey);
+                        String year = String.valueOf(HRDateTimeUtils.getYear(lastEmpPosOrgRelDyo.getDate(SanDingConstant.ENDDATE)));
+                        String key = positionBoId + "@" + year;
+                        List<Long> kgsddEmpPosOrgRelIdList = kgsddEmpPosOrgRelIdListMap.get(key);
+                        if (HRObjectUtils.isEmpty(kgsddEmpPosOrgRelIdList)) {
+                            kgsddEmpPosOrgRelIdList = new ArrayList<Long>();
+                            kgsddEmpPosOrgRelIdList.add(positionBoId);
+                            kgsddEmpPosOrgRelIdListMap.put(key, kgsddEmpPosOrgRelIdList);
+                        } else {
+                            kgsddEmpPosOrgRelIdList.add(positionBoId);
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+        // 处理成 key->岗位BOID@年 value->数量
+        Map<String, Integer> kgsddEmpPosOrgRelMap = kgsddEmpPosOrgRelIdListMap.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().size()));
+        // key->单位BOID@岗位BOID value->定员数
+        Map<String, Integer> lastSanDingTaskMap = new HashMap<String, Integer>();
+        DynamicObject lastSanDingPlanDyo = getLastSanDingPlanDyo(billDyo.getLong(SanDingConstant.ID_KEY), SanDingConstant.ID_KEY);
+        if (!HRObjectUtils.isEmpty(lastSanDingPlanDyo)) {
+            long sanDingPlanId = lastSanDingPlanDyo.getLong(SanDingConstant.ID_KEY);
+            // 查询 单据体.定员数、单据体.岗位.BOID、单位.BOID
+            selectFields = QueryFieldBuilder.create()
+                    .add(SanDingConstant.NCKD_ENTRYENTITY, SanDingConstant.NCKD_AUTHORIZEDSTRENGTH_KEY)
+                    .add(SanDingConstant.NCKD_ENTRYENTITY, SanDingConstant.NCKD_POSITION_KEY, SanDingConstant.BOID_KEY)
+                    .add(companyBoIdKey)
+                    .buildSelect();
+            // 获取三定任务ID
+            DynamicObject[] lastSanDingTaskDyos = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGTASK_ENTITY).queryOriginalArray(selectFields, new QFilter[]{new QFilter(String.join(".", SanDingConstant.NCKD_SANDINGPLAN_KEY, SanDingConstant.ID_KEY), QCP.equals, sanDingPlanId)});
+            for (DynamicObject lastSanDingTaskDyo : lastSanDingTaskDyos) {
+                Long positionBoId = lastSanDingTaskDyo.getLong(String.join(".", SanDingConstant.NCKD_ENTRYENTITY, SanDingConstant.NCKD_POSITION_KEY, SanDingConstant.BOID_KEY));
+                int authorizedStrength = lastSanDingTaskDyo.getInt(String.join(".", SanDingConstant.NCKD_ENTRYENTITY, SanDingConstant.NCKD_AUTHORIZEDSTRENGTH_KEY));
+                Long companyBoId = lastSanDingTaskDyo.getLong(companyBoIdKey);
+                lastSanDingTaskMap.put(companyBoId+"@"+positionBoId, authorizedStrength);
+            }
+        }
+        // 构建三定任务
+        HRBaseServiceHelper sanDingTaskServiceHelper = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGTASK_ENTITY);
+        List<DynamicObject> sanDingTaskDyoList = new ArrayList<DynamicObject>();
+        for (DynamicObject entryDyo : entryDyoColl) {
+            // 设置 状态为未提交
+            entryDyo.set(SanDingConstant.NCKD_STATUS_KEY, SanDingPlanEntryStatus.UNSUBMIT.getCode());
+            // 构建 三定任务
+            DynamicObject sanDingTaskDyo = sanDingTaskServiceHelper.generateEmptyDynamicObject();
+            // 设置 ID
+            sanDingTaskDyo.set(SanDingConstant.ID_KEY, ORM_IMPL.genLongId(sanDingTaskDyo.getDataEntityType()));
+            // 设置 创建人
+            sanDingTaskDyo.set(SanDingConstant.CREATOR_KEY, UserServiceHelper.getCurrentUserId());
+            // 设置 主业务组织
+            sanDingTaskDyo.set(SanDingConstant.ORG_KEY, OrgHelper.getCreateOrg(SanDingConstant.NCKD_SANDINGTASK_ENTITY));
+            // 设置 单据状态
+            sanDingTaskDyo.set(SanDingConstant.BILL_STATUS_KEY, "A");// 默认暂存
+            // 设置 审批状态
+            sanDingTaskDyo.set(SanDingConstant.AUDIT_STATUS, "A");// 默认暂存
+            // 设置 单据编码
+            CodeRuleInfo codeRuleInfo = CodeRuleServiceHelper.getCodeRule(SanDingConstant.NCKD_SANDINGTASK_ENTITY, sanDingTaskDyo, OrgUtil.getMainOrgId(sanDingTaskDyo));
+            if (codeRuleInfo == null) {
+                throw new KDBizException("三定任务没有可使用的编码规则");
+            }
+            String number = CodeRuleServiceHelper.readNumber(codeRuleInfo, sanDingTaskDyo);
+            sanDingTaskDyo.set(SanDingConstant.BILL_NO_KEY,  number);
+            // 设置 三定计划
+            sanDingTaskDyo.set(SanDingConstant.NCKD_SANDINGPLAN_KEY, billDyo);
+            // 设置 三定计划分录
+            sanDingTaskDyo.set(SanDingConstant.NCKD_SDP_ENTRY_KEY, entryDyo);
+            // 设置 单位
+            sanDingTaskDyo.set(SanDingConstant.NCKD_COMPANY_KEY, entryDyo.getLong(String.join(".", SanDingConstant.NCKD_COMPANY_KEY, SanDingConstant.ID_KEY)));
+            // 构建单据体
+            DynamicObjectCollection sanDingTaskEntryDyoColl = sanDingTaskDyo.getDynamicObjectCollection(SanDingConstant.NCKD_ENTRYENTITY);
+            List<Map<String, Long>> positionBoIdList = companyPositionMap.get(entryDyo.getLong(companyBoIdKey));
+            for (Map<String, Long> positionOrgMap : positionBoIdList) {
+                DynamicObject sanDingTaskEntryDyo = sanDingTaskEntryDyoColl.addNew();
+                // 岗位BOID
+                Long positionBoId = positionOrgMap.get("positionBoId");
+                // 设置 组织
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_ADMINORG, positionOrgMap.get("adminOrgSourceVid"));
+                // 设置 岗位
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_POSITION_KEY, positionOrgMap.get("positionSourceVid"));
+                // 设置 定员数
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_AUTHORIZEDSTRENGTH_KEY, lastSanDingTaskMap.getOrDefault(entryDyo.getLong(companyBoIdKey)+"@"+positionBoId, 0));
+                // 设置 实际占编人数
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_ACTUALSTAFFCOUNT_KEY, positionInStaffEmployeeCountMap.getOrDefault(positionBoId, 0));
+                // 设置 缺编人数
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_STAFFINGSHORTFALL_KEY, 0);
+                // 设置 前一年流失人数(系统数)
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_Y1OUT_SYS_KEY, y1OutSysMap.getOrDefault(positionBoId + "@" + y1, 0) + kgsddEmpPosOrgRelMap.getOrDefault(positionBoId + "@" + y1, 0));
+                // 设置 前两年流失人数(系统数)
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_Y2OUT_SYS_KEY, y2OutSysMap.getOrDefault(positionBoId + "@" + y2, 0) + kgsddEmpPosOrgRelMap.getOrDefault(positionBoId + "@" + y2, 0));
+                // 设置 前三年流失人数(系统数)
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_Y3OUT_SYS_KEY, y3OutSysMap.getOrDefault(positionBoId + "@" + y3, 0) + kgsddEmpPosOrgRelMap.getOrDefault(positionBoId + "@" + y3, 0));
+                // 设置 前一年流失人数(确认数)
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_Y1OUT_CONF_KEY, sanDingTaskEntryDyo.get(SanDingConstant.NCKD_Y1OUT_SYS_KEY));
+                // 设置 前两年流失人数(确认数)
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_Y2OUT_CONF_KEY, sanDingTaskEntryDyo.get(SanDingConstant.NCKD_Y2OUT_SYS_KEY));
+                // 设置 前三年流失人数(确认数)
+                sanDingTaskEntryDyo.set(SanDingConstant.NCKD_Y3OUT_CONF_KEY, sanDingTaskEntryDyo.get(SanDingConstant.NCKD_Y3OUT_SYS_KEY));
+            }
+            sanDingTaskDyoList.add(sanDingTaskDyo);
+            SANDING_TASK_MAP.put(entryDyo.getLong(SanDingConstant.ID_KEY), sanDingTaskDyo.getLong(SanDingConstant.ID_KEY));
+        }
+        // 保存
+        OperationResult operationResult = SaveServiceHelper.saveOperate(SanDingConstant.NCKD_SANDINGTASK_ENTITY, sanDingTaskDyoList.toArray(new DynamicObject[0]));
+        if (operationResult != null && !operationResult.isSuccess()) {
+            StringJoiner errorMsg = new StringJoiner(";");
+            for (IOperateInfo error : operationResult.getAllErrorOrValidateInfo()) {
+                errorMsg.add(error.getMessage());
+            }
+            if (!HRObjectUtils.isEmpty(operationResult.getMessage())) {
+                errorMsg.add(operationResult.getMessage());
+            }
+            throw new KDBizException(errorMsg.toString());
+        }
+    }
+
+    private static DynamicObject getLastSanDingPlanDyo(Long id, String selectFields) {
+        // 排序 年度降序、适用批次人力资源需求批次.编码降序
+        String orderBys = QueryFieldBuilder.create().addDesc(SanDingConstant.NCKD_SANDINGYEAR_KEY).addDesc(SanDingConstant.NCKD_SANDINGTIME_KEY, SanDingConstant.NUMBER_KEY).buildOrder();
+        // 获取最近一个三定计划
+        DynamicObject[] lastSanDingPlanDyos = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGPLAN_ENTITY).queryOriginalArray(selectFields, new QFilter[]{new QFilter(SanDingConstant.ID_KEY, QCP.not_equals, id)}, orderBys);
+        if (lastSanDingPlanDyos.length > 0) {
+            return HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGPLAN_ENTITY).loadSingle(lastSanDingPlanDyos[0].getLong(SanDingConstant.ID_KEY));
+        }
+        return null;
+    }
+
+    @Override
+    public void endOperationTransaction(EndOperationTransactionArgs e) {
+        super.endOperationTransaction(e);
+
+        try {
+            DynamicObject[] dyos = e.getDataEntities();
+            for (DynamicObject dyo : dyos) {
+                DynamicObjectCollection entryDyoColl = dyo.getDynamicObjectCollection(SanDingConstant.NCKD_ENTRYENTITY);
+                for (DynamicObject entryDyo : entryDyoColl) {
+                    sendMessage(entryDyo);
+                }
+                updateLastSanDingPlanEndDate(dyo);
+            }
+        } catch (Exception ex) {
+            throw new KDBizException(ex, new ErrorCode("sendMessageError", ex.getMessage()));
+        }
+    }
+
+    private void sendMessage(DynamicObject entryDyo) {
+        // 构建 消息
+        MessageInfo messageInfo = new MessageInfo();
+        // 设置 消息标题
+        messageInfo.setMessageTitle(new LocaleString("三定任务填报提醒"));
+        // 设置 文本消息内容
+        messageInfo.setMessageContent(new LocaleString("您有一个三定任务待填报,请尽快处理,点击下方【快速处理】链接处理。"));
+        // 设置 消息类型
+        messageInfo.setType(MessageInfo.TYPE_MESSAGE);
+        // 设置 实体编码
+        messageInfo.setEntityNumber(SanDingConstant.NCKD_SANDINGTASK_ENTITY);
+        // 设置 标签
+        messageInfo.setTag("三定管理");
+        long managerBoId = entryDyo.getDynamicObject(SanDingConstant.NCKD_MANAGER_KEY).getLong(SanDingConstant.BOID_KEY);
+        // 设置 消息接收人
+//        List<Long> userIds = getUserIds(Collections.singletonList(managerBoId));
+        List<Long> userIds = new ArrayList<Long>();
+        userIds.add(2306610598445593600L);
+        messageInfo.setUserIds(userIds);
+        // 设置 消息Web端url
+        messageInfo.setContentUrl(UrlService.getDomainContextUrl() + "/index.html?formId="+SanDingConstant.NCKD_SANDINGTASK_ENTITY+"&pkId=" + SANDING_TASK_MAP.get(entryDyo.getLong(SanDingConstant.ID_KEY)));
+        MessageUtils.sendMessage(messageInfo);
+    }
+
+    private List<Long> getUserIds(List<Long> managerBoIdList) {
+        // 获取 所有负责人userId
+        Map<String, Object> result = HRPIEmployeeServiceHelper.queryUserIdsByEmployeeIds(managerBoIdList, null);
+        if ((Boolean) result.get("success")) {
+            Map<Long, Map<String, Object>> retMap = (Map<Long, Map<String, Object>>) result.get("data");
+            // 从返回结果中提取用户ID列表
+            List<Long> userIds = retMap.values().stream().map(reMap -> (Long) reMap.get("user")).collect(Collectors.toList());
+            return userIds;
+        } else {
+            throw new KDBizException((String) result.get("message"));
+        }
+    }
+
+    private void updateLastSanDingPlanEndDate(DynamicObject dyo) {
+        Date startDate = dyo.getDate(SanDingConstant.NCKD_STARTDATE_KEY);
+        DynamicObject lastSanDingPlanDyo = getLastSanDingPlanDyo(dyo.getLong(SanDingConstant.ID_KEY), String.join(",", SanDingConstant.ID_KEY, SanDingConstant.NCKD_ENDDATE));
+        if (!HRObjectUtils.isEmpty(lastSanDingPlanDyo)) {
+            lastSanDingPlanDyo.set(SanDingConstant.NCKD_ENDDATE, HRDateTimeUtils.addDay(startDate, -1));
+            SaveServiceHelper.save(new DynamicObject[]{lastSanDingPlanDyo});
+        }
+    }
+
+}

+ 78 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/operate/UpdateSanDingPlanOpPlugin.java

@@ -0,0 +1,78 @@
+package nckd.jxccl.hr.sdm.plugin.operate;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
+import kd.bos.entity.plugin.PreparePropertysEventArgs;
+import kd.bos.entity.plugin.args.EndOperationTransactionArgs;
+import kd.bos.orm.query.QCP;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.hr.hbp.business.servicehelper.HRBaseServiceHelper;
+import kd.hr.hbp.common.util.HRStringUtils;
+import nckd.jxccl.hr.sdm.common.SanDingConstant;
+import nckd.jxccl.hr.sdm.common.SanDingPlanEntryStatus;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 更新三定计划操作插件
+ * @entity: nckd_sandingtask
+ * @operat: submit、unsubmit、audit
+ * @author: jtd
+ * @date: 2025/12/12 17:05
+ */
+public class UpdateSanDingPlanOpPlugin extends AbstractOperationServicePlugIn {
+
+    @Override
+    public void onPreparePropertys(PreparePropertysEventArgs e) {
+        super.onPreparePropertys(e);
+
+        e.getFieldKeys().addAll(billEntityType.getAllFields().keySet());
+    }
+
+    @Override
+    public void endOperationTransaction(EndOperationTransactionArgs e) {
+        super.endOperationTransaction(e);
+
+        String operationKey = e.getOperationKey();
+        DynamicObject[] dyos = e.getDataEntities();
+        List<Long> sanDingPlanEntryIdList = Arrays.stream(dyos).map(dyo -> dyo.getLong(String.join(".", SanDingConstant.NCKD_SDP_ENTRY_KEY, SanDingConstant.ID_KEY))).collect(Collectors.toList());
+        DynamicObject[] sanDingPlanEntryDyos = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGPLAN_ENTRY_ENTITY).load(SanDingConstant.NCKD_STATUS_KEY, new QFilter[]{new QFilter(SanDingConstant.ID_KEY, QCP.in, sanDingPlanEntryIdList)});
+        for (DynamicObject sanDingPlanEntryDyo : sanDingPlanEntryDyos) {
+            switch (operationKey) {
+                case SanDingConstant.SUBMIT_OP:
+                    sanDingPlanEntryDyo.set(SanDingConstant.NCKD_STATUS_KEY, SanDingPlanEntryStatus.SUBMIT.getCode());
+                    break;
+                case SanDingConstant.UNSUBMIT_OP:
+                    sanDingPlanEntryDyo.set(SanDingConstant.NCKD_STATUS_KEY, SanDingPlanEntryStatus.UNSUBMIT.getCode());
+                    break;
+                case SanDingConstant.AUDIT_OP:
+                    sanDingPlanEntryDyo.set(SanDingConstant.NCKD_STATUS_KEY, SanDingPlanEntryStatus.COMPLETED.getCode());
+            }
+        }
+        SaveServiceHelper.save(sanDingPlanEntryDyos);
+        // 判断该计划下的所有任务是否都已完成
+        if (HRStringUtils.equals(SanDingConstant.AUDIT_OP, operationKey)) {
+            List<Long> sanDingPlanIdList = Arrays.stream(dyos).map(dyo -> dyo.getLong(String.join(".", SanDingConstant.NCKD_SANDINGPLAN_KEY, SanDingConstant.ID_KEY))).collect(Collectors.toList());
+            // 过滤 三定计划ID 并且 单据体.状态<>已完成
+            QFilter qFilter = new QFilter(SanDingConstant.ID_KEY, QCP.in, sanDingPlanIdList)
+                    .and(String.join(".", SanDingConstant.NCKD_ENTRYENTITY, SanDingConstant.NCKD_STATUS_KEY), QCP.not_equals, SanDingPlanEntryStatus.COMPLETED.getCode());
+            // 查询出三定计划中存在未完成三定任务的数据
+            HRBaseServiceHelper sanDingPlanServiceHelper = HRBaseServiceHelper.create(SanDingConstant.NCKD_SANDINGPLAN_ENTITY);
+            DynamicObject[] sanDingPlanUnComplateDyos = sanDingPlanServiceHelper.queryOriginalArray(SanDingConstant.ID_KEY, new QFilter[]{qFilter});
+            List<Long> sanDingPlanUnComplateIdList = Arrays.stream(sanDingPlanUnComplateDyos).map(dyo -> dyo.getLong(SanDingConstant.ID_KEY)).collect(Collectors.toList());
+            // 剔除
+            sanDingPlanIdList.removeAll(sanDingPlanUnComplateIdList);
+            if (sanDingPlanIdList.size() > 0) {
+                DynamicObject[] sanDingPlanDyos = sanDingPlanServiceHelper.load(SanDingConstant.NCKD_ISCOMPLATED_KEY, new QFilter[]{new QFilter(SanDingConstant.ID_KEY, QCP.in, sanDingPlanIdList)});
+                for (DynamicObject sanDingPlanDyo : sanDingPlanDyos) {
+                    sanDingPlanDyo.set(SanDingConstant.NCKD_ISCOMPLATED_KEY, Boolean.TRUE);
+                }
+                SaveServiceHelper.save(sanDingPlanDyos);
+            }
+        }
+    }
+
+}

+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/other/.gitkeep


+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/report/.gitkeep


+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/plugin/workflow/.gitkeep


+ 0 - 0
code/hr/nckd-jxccl-hr/src/main/java/nckd/jxccl/hr/sdm/webapi/.gitkeep