Quellcode durchsuchen

出差申请单新增开发

dingsixi vor 1 Monat
Ursprung
Commit
0dea44a283

+ 223 - 0
base/nckd-base-common/src/main/java/nckd/base/common/utils/OperationUtils.java

@@ -0,0 +1,223 @@
+package nckd.base.common.utils;
+
+import kd.bos.dataentity.OperateOption;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.metadata.dynamicobject.DynamicObjectType;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.operate.result.IOperateInfo;
+import kd.bos.entity.operate.result.OperationResult;
+import kd.bos.exception.KDBizException;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.operation.OperationServiceHelper;
+import kd.fi.er.common.constant.page.ErTripReqBillConstant;
+import nckd.base.common.constant.BaseFieldConst;
+import nckd.base.common.constant.OperationKeyConst;
+import nckd.base.common.enums.BillStatusEnum;
+import org.apache.commons.lang3.ObjectUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author ZHANGT
+ * @module 销售管理平台-XX应用-XX模块-XX单据
+ * @description 单据操作类
+ * @since 2025/3/26
+ */
+public class OperationUtils {
+    private OperationUtils() {
+    }
+
+    /**
+     * 单据保存到审核
+     * @param bills  单据对象
+     * @param formBillId 单据标识
+     */
+    public static void operationDoSave2Audit(DynamicObject[] bills, String formBillId) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        OperationResult saveResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.SAVE,
+                formBillId,
+                bills,
+                OperateOption.create()
+        );
+        validateOperationResult(saveResult, String.format("新增【%s】失败:", type.getDisplayName().toString()));
+        OperationResult submitResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.SUBMIT,
+                formBillId,
+                saveResult.getSuccessPkIds().toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(submitResult, String.format("提交【%s】失败:", type.getDisplayName().toString()));
+        OperateOption operateOption = OperateOption.create();
+        //出差申请单跳过审批流
+        if(formBillId.equals("er_tripreqbill")){
+            operateOption.setVariableValue("WF","false");
+        }
+        OperationResult auditResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.AUDIT,
+                formBillId,
+                submitResult.getSuccessPkIds().toArray(),
+                operateOption
+        );
+        validateOperationResult(auditResult, String.format("审核【%s】失败:", type.getDisplayName().toString()));
+    }
+
+    public static void operationDoSubmit(String formBillId, Set<Object> ids) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        // 提交
+        OperationResult submitResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.SUBMIT,
+                formBillId,
+                ids.toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(submitResult, String.format("提交【%s】失败:", type.getDisplayName().toString()));
+    }
+
+    public static void operationDoAudit(String formBillId, Set<Object> ids) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        // 审核
+        OperationResult auditResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.AUDIT,
+                formBillId,
+                ids.toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(auditResult, String.format("审核【%s】失败:", type.getDisplayName().toString()));
+    }
+
+    public static void operationDoUnAudit(String formBillId, Set<Object> ids) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        // 反审核
+        OperationResult unAuditResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.UN_AUDIT,
+                formBillId,
+                ids.toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(unAuditResult, String.format("反审核【%s】失败:", type.getDisplayName().toString()));
+    }
+
+    
+
+
+    public static void operationDoSubmit2Audit(String formBillId, Set<Object> ids) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        // 提交
+        OperationResult submitResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.SUBMIT,
+                formBillId,
+                ids.toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(submitResult, String.format("提交【%s】失败:", type.getDisplayName().toString()));
+
+        // 审核
+        OperationResult auditResult = OperationServiceHelper.executeOperate(
+                OperationKeyConst.AUDIT,
+                formBillId,
+                submitResult.getSuccessPkIds().toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(auditResult, String.format("审核【%s】失败:", type.getDisplayName().toString()));
+    }
+
+    public static void operationDoUnAudit2Delete(String formBillId, Set<Object> ids) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        DynamicObject[] arr = BusinessDataServiceHelper.load(ids.toArray(), type);
+        operationDoUnAudit2Delete(arr, formBillId);
+    }
+
+    public static void operationDoUnAudit2Delete(DynamicObject[] bills, String formBillId) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        String statusKey = BaseFieldConst.BILL_STATUS;
+        if (type.getProperty(BaseFieldConst.STATUS) != null) {
+            statusKey = BaseFieldConst.STATUS;
+        }
+
+        List<Object> unAuditList = new ArrayList<>();
+        List<Object> unSubmitList = new ArrayList<>();
+        List<Object> deleteIdList = new ArrayList<>();
+        for (DynamicObject bill : bills) {
+            Object pkValue = bill.getPkValue();
+            String status = bill.getString(statusKey);
+            if (BillStatusEnum.AUDIT.getValue().equals(status)) {
+                unAuditList.add(pkValue);
+            }
+            if (BillStatusEnum.SUBMIT.getValue().equals(status)) {
+                unSubmitList.add(pkValue);
+            } else {
+                deleteIdList.add(pkValue);
+            }
+        }
+        // 反审核
+        if (!ObjectUtils.isEmpty(unAuditList)) {
+            OperationResult unAuditResult = OperationServiceHelper.executeOperate(
+                    OperationKeyConst.UN_AUDIT,
+                    formBillId,
+                    unAuditList.toArray(),
+                    OperateOption.create()
+            );
+            validateOperationResult(unAuditResult, String.format("反审核【%s】失败:", type.getDisplayName().toString()));
+            deleteIdList.addAll(unAuditResult.getSuccessPkIds());
+        }
+
+        // 撤销
+        if (!ObjectUtils.isEmpty(unSubmitList)) {
+            OperationResult unSubmitResult = OperationServiceHelper.executeOperate(
+                    OperationKeyConst.UN_SUBMIT,
+                    formBillId,
+                    unSubmitList.toArray(),
+                    OperateOption.create()
+            );
+            validateOperationResult(unSubmitResult, String.format("撤销【%s】失败:", type.getDisplayName().toString()));
+            deleteIdList.addAll(unSubmitResult.getSuccessPkIds());
+        }
+
+        // 删除
+        if (!ObjectUtils.isEmpty(deleteIdList)) {
+            OperationResult deleteResult = OperationServiceHelper.executeOperate(
+                    OperationKeyConst.DELETE,
+                    formBillId,
+                    deleteIdList.toArray(),
+                    OperateOption.create()
+            );
+            validateOperationResult(deleteResult, String.format("删除【%s】失败:", type.getDisplayName().toString()));
+        }
+    }
+
+    public static void operationDoMethod(String formBillId,String method, Set<Object> ids) {
+        DynamicObjectType type = EntityMetadataCache.getDataEntityType(formBillId);
+        // 提交
+        OperationResult submitResult = OperationServiceHelper.executeOperate(
+                method,
+                formBillId,
+                ids.toArray(),
+                OperateOption.create()
+        );
+        validateOperationResult(submitResult, String.format("操作【%s】失败:", type.getDisplayName().toString()));
+    }
+
+    /**
+     * 验证操作结果
+     *
+     * @param result
+     * @param errorPrefix
+     */
+    public static void validateOperationResult(OperationResult result, String errorPrefix) {
+        if (!result.isSuccess()) {
+            String errorMsg = result.getMessage();
+            List<IOperateInfo> allErrorOrValidateInfo = result.getAllErrorOrValidateInfo();
+            if (ObjectUtils.isNotEmpty(allErrorOrValidateInfo)) {
+                String errorMsg2 = allErrorOrValidateInfo.stream()
+                        .map(IOperateInfo::getMessage)
+                        .collect(Collectors.joining(","));
+                errorMsg = errorMsg + errorMsg2;
+            }
+            throw new KDBizException(errorPrefix + errorMsg);
+        }
+    }
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 483 - 0
nckd-fi/src/main/java/nckd/fi/er/common/constant/TripReqBillConstant.java


+ 367 - 0
nckd-fi/src/main/java/nckd/fi/er/common/utils/TripReqBillUtils.java

@@ -0,0 +1,367 @@
+package nckd.fi.er.common.utils;
+
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.MainEntityType;
+import kd.bos.orm.query.QFilter;
+import kd.bos.servicehelper.BusinessDataServiceHelper;
+import kd.bos.servicehelper.QueryServiceHelper;
+import nckd.base.common.constant.BaseFieldConst;
+import nckd.fi.er.webapi.model.TripEntryModel;
+import nckd.fi.er.webapi.model.TripReqBillModel;
+import org.apache.commons.lang3.ObjectUtils;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @description:出差申请单api接口工具类
+ * @author: dingsixi
+ * @create: 2025/12/12 11:22
+ */
+public class TripReqBillUtils {
+
+    /**
+     * 交通工具
+     */
+    private final static Map<String, String> validVehicleMap = new HashMap<String, String>() {{
+        put("1", "飞机");
+        put("2", "火车");
+        put("3", "汽车");
+        put("4", "轮船");
+        put("5", "其他工具");
+        put("6", "派车");
+    }};
+
+    /**
+     * @param type
+     * @param currency
+     * @param expenseItem
+     * @return 校验默认值
+     */
+    public static List<String> validateDefaultValue(DynamicObject type, DynamicObject currency, DynamicObject expenseItem) {
+        List<String> errorMessages = new ArrayList<>();
+        if (null == type) {
+            errorMessages.add("未查询到报销类型编码等于03的常规报销,请联系星瀚系统管理员维护相应基础资料");
+        }
+        if (null == currency) {
+            errorMessages.add("未查询到币别编码等于CNY的人民币,请联系星瀚系统管理员维护相应基础资料");
+        }
+        if (null == expenseItem) {
+            errorMessages.add("未查询到费用项目编码等于16.001的差旅费,请联系星瀚系统管理员维护相应基础资料");
+        }
+        return errorMessages;
+    }
+
+    /**
+     * @param numbers    基础资料编码
+     * @param objMap     通过编码查询出的基础资料数据
+     * @param entityName 基础资料名称
+     * @return 返回不为空说明部分编码未找到基础数据
+     */
+    public static String validateBaseDataIsExists(List<String> numbers, Map<String, DynamicObject> objMap, String entityName) {
+        Set<String> numberSet = objMap.keySet();
+        numbers.removeAll(numberSet);
+        if (!numbers.isEmpty()) {
+            return entityName + "编码为" + String.join("、", numbers) + "的数据不存在,无法新增";
+        }
+        return "";
+    }
+
+    /**
+     * 校验出差申请单列表
+     *
+     * @param tripReqBillModelList 出差申请单列表
+     * @return 错误消息列表,为空表示校验通过
+     */
+    public static List<String> validateTripReqBillModels(List<TripReqBillModel> tripReqBillModelList) {
+        List<String> errorMessages = new ArrayList<>();
+
+        // 1. 校验主列表
+        if (ObjectUtils.isEmpty(tripReqBillModelList)) {
+            errorMessages.add("出差申请单列表不能为空");
+            return errorMessages;
+        }
+
+        // 2. 遍历校验每个出差申请单
+        for (int i = 0; i < tripReqBillModelList.size(); i++) {
+            TripReqBillModel billModel = tripReqBillModelList.get(i);
+            String billPrefix = "第" + (i + 1) + "张出差申请单";
+
+            // 2.1 校验TripReqBillModel本身
+            List<String> billErrors = validateTripReqBillModel(billModel, billPrefix);
+            errorMessages.addAll(billErrors);
+
+            // 2.2 校验行程信息列表
+            List<TripEntryModel> tripEntryList = billModel.getTripEntryModelList();
+            if (ObjectUtils.isEmpty(tripEntryList)) {
+                errorMessages.add(billPrefix + "的行程信息不能为空");
+            } else {
+                // 2.3 校验每个TripEntryModel
+                List<String> entryErrors = validateTripEntryModelList(tripEntryList, billPrefix);
+                errorMessages.addAll(entryErrors);
+
+                // 2.4 校验跨层级业务逻辑
+                List<String> logicErrors = validateCrossLevelLogic(billModel, tripEntryList, billPrefix);
+                errorMessages.addAll(logicErrors);
+            }
+        }
+
+        return errorMessages;
+    }
+
+    /**
+     * 校验单个TripReqBillModel
+     */
+    private static List<String> validateTripReqBillModel(TripReqBillModel model, String prefix) {
+        List<String> errorMessages = new ArrayList<>();
+
+        // 校验基本必填字段
+        if (ObjectUtils.isEmpty(model.getApplier())) {
+            errorMessages.add(prefix + "的申请人不能为空");
+        }
+        if (ObjectUtils.isEmpty(model.getApplyDate())) {
+            errorMessages.add(prefix + "的申请日期不能为空");
+        }
+        if (ObjectUtils.isEmpty(model.getCostCompany())) {
+            errorMessages.add(prefix + "的费用承担公司不能为空");
+        }
+        if (ObjectUtils.isEmpty(model.getCostDept())) {
+            errorMessages.add(prefix + "的费用承担部门不能为空");
+        }
+        if (ObjectUtils.isEmpty(model.getApplierPosition())) {
+            errorMessages.add(prefix + "的职位不能为空");
+        }
+
+        // 校验多出差人逻辑
+        if (ObjectUtils.isEmpty(model.getMultiTravelers())) {
+            errorMessages.add(prefix + "的多出差人不能为空");
+        }
+
+        // 校验日期格式
+        if (model.getApplyDate() != null && !isValidDate(model.getApplyDate())) {
+            errorMessages.add(prefix + "的申请日期格式不正确,应为YYYY-MM-DD格式");
+        }
+
+        return errorMessages;
+    }
+
+    /**
+     * 校验TripEntryModel列表
+     */
+    private static List<String> validateTripEntryModelList(List<TripEntryModel> entryList, String billPrefix) {
+        List<String> errorMessages = new ArrayList<>();
+
+        for (int j = 0; j < entryList.size(); j++) {
+            TripEntryModel entry = entryList.get(j);
+            String entryPrefix = billPrefix + "的第" + (j + 1) + "条行程信息";
+
+            // 校验基本必填字段
+            if (ObjectUtils.isEmpty(entry.getTravelers())) {
+                errorMessages.add(entryPrefix + "的出差人不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getFrom())) {
+                errorMessages.add(entryPrefix + "的出发地不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getTo())) {
+                errorMessages.add(entryPrefix + "的目的地不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getStartDate())) {
+                errorMessages.add(entryPrefix + "的开始日期不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getEndDate())) {
+                errorMessages.add(entryPrefix + "的结束日期不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getVehicle())) {
+                errorMessages.add(entryPrefix + "的交通工具不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getCostCompany())) {
+                errorMessages.add(entryPrefix + "的费用承担公司不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getCostDept())) {
+                errorMessages.add(entryPrefix + "的费用承担部门不能为空");
+            }
+            if (ObjectUtils.isEmpty(entry.getTripOriAmount())) {
+                errorMessages.add(entryPrefix + "的申请金额不能为空");
+            }
+
+            // 校验业务逻辑
+            List<String> logicErrors = validateTripEntryModelLogic(entry, entryPrefix);
+            errorMessages.addAll(logicErrors);
+        }
+
+        return errorMessages;
+    }
+
+    /**
+     * 校验TripEntryModel的业务逻辑
+     */
+    private static List<String> validateTripEntryModelLogic(TripEntryModel entry, String entryPrefix) {
+        List<String> errorMessages = new ArrayList<>();
+
+        // 校验金额
+        if (entry.getTripOriAmount() != null) {
+            if (entry.getTripOriAmount().compareTo(BigDecimal.ZERO) <= 0) {
+                errorMessages.add(entryPrefix + "的申请金额不能小于等于0");
+            }
+        }
+
+        // 校验日期
+        String startDate = entry.getStartDate();
+        String endDate = entry.getEndDate();
+
+        if (startDate != null && endDate != null) {
+            // 校验日期格式
+            if (!isValidDate(startDate)) {
+                errorMessages.add(entryPrefix + "的开始日期格式不正确,应为YYYY-MM-DD格式");
+            }
+            if (!isValidDate(endDate)) {
+                errorMessages.add(entryPrefix + "的结束日期格式不正确,应为YYYY-MM-DD格式");
+            }
+
+            // 如果格式正确,校验日期逻辑
+            if (isValidDate(startDate) && isValidDate(endDate)) {
+                try {
+                    java.time.LocalDate start = java.time.LocalDate.parse(startDate);
+                    java.time.LocalDate end = java.time.LocalDate.parse(endDate);
+
+                    if (end.isBefore(start)) {
+                        errorMessages.add(entryPrefix + "的结束日期不能早于开始日期");
+                    }
+
+                    // 校验行程天数
+                    long days = java.time.temporal.ChronoUnit.DAYS.between(start, end);
+                    if (days < 0) {
+                        errorMessages.add(entryPrefix + "的行程天数不能为负数");
+                    }
+                } catch (Exception e) {
+                    errorMessages.add(entryPrefix + "的日期格式解析错误");
+                }
+            }
+        }
+
+
+        // 校验交通工具
+        if (entry.getVehicle() != null) {
+            String[] split = entry.getVehicle().split(",");
+            for (int i = 0; i < split.length; i++) {
+                String s = split[i];
+                if (!validVehicleMap.containsKey(s)) {
+                    errorMessages.add(entryPrefix + "的交通工具必须是: " + String.join("、", validVehicleMap.values()));
+                }
+            }
+
+        }
+
+        return errorMessages;
+    }
+
+    /**
+     * 校验跨层级业务逻辑
+     */
+    private static List<String> validateCrossLevelLogic(TripReqBillModel billModel,
+                                                        List<TripEntryModel> entryList,
+                                                        String billPrefix) {
+        List<String> errorMessages = new ArrayList<>();
+
+        // 1. 校验行程中的出差人是否在多出差人列表中
+        if (Boolean.TRUE.equals(billModel.getTravelers()) &&
+                billModel.getMultiTravelers() != null) {
+
+            List<String> multiTravelers = billModel.getMultiTravelers();
+
+            for (int j = 0; j < entryList.size(); j++) {
+                TripEntryModel entry = entryList.get(j);
+                String traveler = entry.getTravelers();
+
+                if (traveler != null && !traveler.trim().isEmpty() &&
+                        !multiTravelers.contains(traveler)) {
+                    errorMessages.add(billPrefix + "的第" + (j + 1) +
+                            "条行程信息的出差人【" + traveler + "】不在出差人列表中");
+                }
+            }
+        }
+
+        // 2. 校验费用承担公司和部门的一致性(如果需要)
+        // 例如:申请单的费用承担公司/部门应该与所有行程的一致
+//        for (int j = 0; j < entryList.size(); j++) {
+//            TripEntryModel entry = entryList.get(j);
+//            String entryPrefix = billPrefix + "的第" + (j + 1) + "条行程信息";
+//
+//            if (billModel.getCostCompany() != null && entry.getCostCompany() != null &&
+//                    !billModel.getCostCompany().equals(entry.getCostCompany())) {
+//                errorMessages.add(entryPrefix + "的费用承担公司与申请单的费用承担公司不一致");
+//            }
+//
+//            if (billModel.getCostDept() != null && entry.getCostDept() != null &&
+//                    !billModel.getCostDept().equals(entry.getCostDept())) {
+//                errorMessages.add(entryPrefix + "的费用承担部门与申请单的费用承担部门不一致");
+//            }
+//        }
+
+        return errorMessages;
+    }
+
+    /**
+     * 校验日期格式是否为YYYY-MM-DD
+     */
+    private static boolean isValidDate(String dateStr) {
+        if (dateStr == null || dateStr.trim().isEmpty()) {
+            return false;
+        }
+        return dateStr.matches("\\d{4}-\\d{2}-\\d{2}");
+    }
+
+
+    /**
+     * @param entity   实体
+     * @param property 过滤字段
+     * @param cp       比较符
+     * @param value    过滤值
+     * @return 查询实体编码对应的id
+     */
+    public static DynamicObject getEntityIdByNumber(String entity, String property, String cp, Object value) {
+        return BusinessDataServiceHelper.loadSingle(entity, new QFilter(property, cp, value).toArray());
+    }
+
+    /**
+     * @param entity   实体标识
+     * @param property 过滤字段
+     * @param cp       比较符
+     * @param value    过滤值
+     * @return 查询对应实体的编码id
+     */
+    public static Map<String, DynamicObject> getEntityIdsByNumbers(String entity, String property, String cp, Object value) {
+        DynamicObjectCollection objectCol = QueryServiceHelper.query(entity, BaseFieldConst.ID, new QFilter(property, cp, value).toArray());
+        if (ObjectUtils.isEmpty(objectCol)) {
+            return Collections.emptyMap();
+        }
+
+        List<Long> idList = objectCol.stream().map(it -> it.getLong(BaseFieldConst.ID)).collect(Collectors.toList());
+        MainEntityType entityType = EntityMetadataCache.getDataEntityType(entity);
+        //先查id后查全量,不知道出差申请单要引用什么属性
+        DynamicObject[] load = BusinessDataServiceHelper.load(idList.toArray(new Long[0]), entityType);
+
+        return Arrays.stream(load)
+                .collect(Collectors.toMap(
+                        it -> it.getString(BaseFieldConst.NUMBER),
+                        it -> it
+                ));
+    }
+
+    /**
+     * 合并String值
+     *
+     * @param lists
+     * @return
+     */
+    public static List<String> mergeLists(List<String>... lists) {
+        List<String> result = new ArrayList<>();
+        for (List<String> list : lists) {
+            result.addAll(list);
+        }
+        return result;
+    }
+}

+ 275 - 0
nckd-fi/src/main/java/nckd/fi/er/webapi/cutomer/TripReqBillWebApiPlugin.java

@@ -0,0 +1,275 @@
+package nckd.fi.er.webapi.cutomer;
+
+import com.alibaba.fastjson.JSONObject;
+import kd.bos.coderule.api.CodeRuleInfo;
+import kd.bos.dataentity.entity.DynamicObject;
+import kd.bos.dataentity.entity.DynamicObjectCollection;
+import kd.bos.entity.EntityMetadataCache;
+import kd.bos.entity.MainEntityType;
+import kd.bos.logging.Log;
+import kd.bos.logging.LogFactory;
+import kd.bos.openapi.common.custom.annotation.ApiController;
+import kd.bos.openapi.common.custom.annotation.ApiMapping;
+import kd.bos.openapi.common.custom.annotation.ApiParam;
+import kd.bos.openapi.common.custom.annotation.ApiPostMapping;
+import kd.bos.openapi.common.result.CustomApiResult;
+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.coderule.CodeRuleServiceHelper;
+import kd.bos.servicehelper.operation.SaveServiceHelper;
+import kd.bos.servicehelper.user.UserServiceHelper;
+import kd.qmc.qcbd.common.constant.BosOrgConst;
+import nckd.base.common.constant.BaseFieldConst;
+import nckd.base.common.constant.BosUserConstant;
+import nckd.base.common.enums.BillStatusEnum;
+import nckd.base.common.utils.DateUtil;
+import nckd.base.common.utils.OperationUtils;
+import nckd.fi.er.common.constant.TripReqBillConstant;
+import nckd.fi.er.common.utils.TripReqBillUtils;
+import nckd.fi.er.webapi.model.TripEntryModel;
+import nckd.fi.er.webapi.model.TripReqBillModel;
+import nckd.fi.er.webapi.model.UserPageModel;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @description:出差申请单接口
+ * @author: dingsixi
+ * @create: 2025/12/11 14:51
+ */
+@ApiController(value = "TripReqBillWebApiPlugin", desc = "出差申请单")
+@ApiMapping(value = "/er_tripreqbill")
+public class TripReqBillWebApiPlugin {
+    private final static Log logger = LogFactory.getLog(TripReqBillWebApiPlugin.class);
+
+
+    /**
+     * 报销类型
+     */
+    private final static String NCKD_AGENTACCOUNTTYPE = "nckd_agentaccounttype";
+
+    @ApiPostMapping(desc = "新增出差申请单", value = "/addNew")
+    public CustomApiResult<Object> addNew(
+            @ApiParam("出差申请") List<TripReqBillModel> tripReqBillModelList) {
+        logger.info("新增出差申请单入参:" + JSONObject.toJSONString(tripReqBillModelList));
+
+        //校验出差申请单入参
+        List<String> errorMessages = TripReqBillUtils.validateTripReqBillModels(tripReqBillModelList);
+        if (!errorMessages.isEmpty()) {
+            return CustomApiResult.fail("500", String.join(",", errorMessages));
+        }
+
+        //报销类型 (默认编码:03  常规报销)
+        DynamicObject type = TripReqBillUtils.getEntityIdByNumber(NCKD_AGENTACCOUNTTYPE, BaseFieldConst.NUMBER, QCP.equals, "03");
+        //币别 默认人民币
+        DynamicObject currency = TripReqBillUtils.getEntityIdByNumber("bd_currency", BaseFieldConst.NUMBER, QCP.equals, "CNY");
+        //费用项目 默认差旅费16.001
+        DynamicObject expenseItem = TripReqBillUtils.getEntityIdByNumber("er_expenseitemedit", BaseFieldConst.NUMBER, QCP.equals, "16.001");
+        //校验默认值
+        errorMessages = TripReqBillUtils.validateDefaultValue(type, currency, expenseItem);
+        if (!errorMessages.isEmpty()) {
+            return CustomApiResult.fail("500", String.join(",", errorMessages));
+        }
+
+        /**提取所有需要查询的基础资料编码,统一查询,只查一次,避免循环*/
+        //所有行程信息
+        List<TripEntryModel> tripEntryModelList = tripReqBillModelList.stream().flatMap(it -> it.getTripEntryModelList().stream()).collect(Collectors.toList());
+
+        //申请人
+        List<String> applierList = tripReqBillModelList.stream().map(TripReqBillModel::getApplier).collect(Collectors.toList());
+        //出差人(行程信息内的出差人,存在多个出差人的情况)
+        List<String> multiTravelersList = tripReqBillModelList.stream().flatMap(it -> it.getMultiTravelers().stream()).collect(Collectors.toList());
+        //人员、出差人
+        List<String> allUserNumberList = TripReqBillUtils.mergeLists(applierList, multiTravelersList);
+
+        //申请人费用承担公司
+        List<String> headCostCompanyList = tripReqBillModelList.stream().map(TripReqBillModel::getCostCompany).collect(Collectors.toList());
+        //申请人费用承担部门
+        List<String> headCostDeptList = tripReqBillModelList.stream().map(TripReqBillModel::getCostDept).collect(Collectors.toList());
+        //行程信息.费用承担部门
+        List<String> costDeptList = tripEntryModelList.stream().map(TripEntryModel::getCostDept).collect(Collectors.toList());
+        //行程信息.费用承担公司
+        List<String> costCompanyList = tripEntryModelList.stream().map(TripEntryModel::getCostCompany).collect(Collectors.toList());
+        //申请人公司、费用承担部门、费用承担公司  合并部门公司编码
+        List<String> allOrgNumberList = TripReqBillUtils.mergeLists(headCostDeptList, headCostCompanyList, costDeptList, costCompanyList);
+
+        //行程信息.成本中心
+        List<String> costCenterList = tripEntryModelList.stream().map(TripEntryModel::getCostCenter).collect(Collectors.toList());
+
+        //行程信息.出发地-目的地
+        List<String> formList = tripEntryModelList.stream().map(TripEntryModel::getFrom).collect(Collectors.toList());
+        List<String> toList = tripEntryModelList.stream().map(TripEntryModel::getTo).collect(Collectors.toList());
+        //合并城市
+        List<String> cityList = TripReqBillUtils.mergeLists(formList, toList);
+
+
+        //人员信息
+        Map<String, DynamicObject> userMap = TripReqBillUtils.getEntityIdsByNumbers(BosUserConstant.FORMBILLID, BaseFieldConst.NUMBER, QCP.in, allUserNumberList);
+        String userMsg = TripReqBillUtils.validateBaseDataIsExists(allUserNumberList, userMap, "人员");
+        if (!userMsg.isEmpty()) {
+            return CustomApiResult.fail("500", userMsg);
+        }
+        //查询所有部门公司编码id
+        Map<String, DynamicObject> orgMap = TripReqBillUtils.getEntityIdsByNumbers(BosOrgConst.BOS_ORG, BaseFieldConst.NUMBER, QCP.in, allOrgNumberList);
+        String orgMsg = TripReqBillUtils.validateBaseDataIsExists(allOrgNumberList, orgMap, "组织");
+        if (!orgMsg.isEmpty()) {
+            return CustomApiResult.fail("500", orgMsg);
+        }
+        //成本中心
+        Map<String, DynamicObject> costCenterMap = TripReqBillUtils.getEntityIdsByNumbers("bos_costcenter", BaseFieldConst.NUMBER, QCP.in, costCenterList);
+        String costCenterMsg = TripReqBillUtils.validateBaseDataIsExists(costCenterList, costCenterMap, "成本中心");
+        if (!costCenterMsg.isEmpty()) {
+            return CustomApiResult.fail("500", costCenterMsg);
+        }
+        //查询所有城市的id
+        Map<String, DynamicObject> cityMap = TripReqBillUtils.getEntityIdsByNumbers("bd_admindivision", BaseFieldConst.NUMBER, QCP.in, cityList);
+        String cityMsg = TripReqBillUtils.validateBaseDataIsExists(cityList, cityMap, "城市");
+        if (!cityMsg.isEmpty()) {
+            return CustomApiResult.fail("500", cityMsg);
+        }
+
+        //创建出差申请单
+        List<DynamicObject> tripReqBillList = createTripReqBill(tripReqBillModelList, currency, type, expenseItem, orgMap, userMap, cityMap, costCenterMap);
+
+        return getResult(tripReqBillList);
+    }
+
+    /**
+     * @param tripReqBillModelList 出差申请单入参
+     * @param currency             币别
+     * @param type                 报销类型
+     * @param expenseItem          费用项目
+     * @param orgMap               组织信息
+     * @param userMap              人员信息
+     * @param cityMap              城市信息
+     * @param costCenterMap        成本中心信息
+     * @return 创建出差申请单
+     */
+    private List<DynamicObject> createTripReqBill(List<TripReqBillModel> tripReqBillModelList, DynamicObject currency,
+                                                  DynamicObject type, DynamicObject expenseItem, Map<String, DynamicObject> orgMap,
+                                                  Map<String, DynamicObject> userMap, Map<String, DynamicObject> cityMap,
+                                                  Map<String, DynamicObject> costCenterMap) {
+        List<DynamicObject> addList = new ArrayList<>();
+        for (TripReqBillModel tripReqBillModel : tripReqBillModelList) {
+
+            //创建出差申请单
+            DynamicObject tripReqBill = BusinessDataServiceHelper.newDynamicObject(TripReqBillConstant.FORMBILLID);
+
+            //单据编号
+            CodeRuleInfo codeRule = CodeRuleServiceHelper.getCodeRule(tripReqBill.getDataEntityType().getName(), tripReqBill, null);
+            tripReqBill.set(TripReqBillConstant.BILLNO, CodeRuleServiceHelper.getNumber(codeRule, tripReqBill));
+
+            //单据状态
+            tripReqBill.set(TripReqBillConstant.BILLSTATUS, BillStatusEnum.SAVE.getValue());
+            //默认非借款
+            tripReqBill.set(TripReqBillConstant.ISLOAN, Boolean.FALSE);
+            //默认人民币
+            tripReqBill.set(TripReqBillConstant.CURRENCY, currency);
+            //职位 主职或兼职  暂定对方传,传不了再通过人员部门查询对应的职位
+            tripReqBill.set(TripReqBillConstant.APPLIERPOSITIONSTR, tripReqBillModel.getApplierPosition());
+            //部门 取费用承担部门
+            tripReqBill.set(TripReqBillConstant.ORG, orgMap.get(tripReqBillModel.getCostDept()));
+
+            //报销类型
+            tripReqBill.set(TripReqBillConstant.NCKD_BXLX, type);
+            //申请人
+            tripReqBill.set(TripReqBillConstant.APPLIER, userMap.get(tripReqBillModel.getApplier()));
+            //出差人(行程信息内的出差人)  存在多个的情况
+            List<String> multiTravelers = tripReqBillModel.getMultiTravelers();
+            DynamicObjectCollection multiTravelersCol = tripReqBill.getDynamicObjectCollection(TripReqBillConstant.MULTITRAVELERS);
+            userMap.entrySet().stream().filter(it -> multiTravelers.contains(it.getKey())).forEach(it -> {
+                multiTravelersCol.addNew().set("fbasedataid", it);
+            });
+            tripReqBill.set(TripReqBillConstant.MULTITRAVELERS, multiTravelersCol);
+            //申请日期
+            tripReqBill.set(TripReqBillConstant.BIZDATE, DateUtil.string2date(tripReqBillModel.getApplyDate(), DateUtil.DATE_FORMAT_YYYY_MM_DD));
+            //是否多出差人
+            tripReqBill.set(TripReqBillConstant.ISTRAVELERS, tripReqBillModel.getTravelers());
+            //事由
+            tripReqBill.set(TripReqBillConstant.DESCRIPTION, tripReqBillModel.getDescription());
+            //申请人公司
+            tripReqBill.set(TripReqBillConstant.COMPANY, orgMap.get(tripReqBillModel.getCostCompany()));
+            //费用承担公司
+            tripReqBill.set(TripReqBillConstant.COSTCOMPANY, orgMap.get(tripReqBillModel.getCostCompany()));
+            //费用承担部门
+            tripReqBill.set(TripReqBillConstant.COSTDEPT, orgMap.get(tripReqBillModel.getCostDept()));
+            //表单id
+            tripReqBill.set(TripReqBillConstant.FORMID, TripReqBillConstant.FORMBILLID);
+            BigDecimal amountCount = BigDecimal.ZERO;
+            DynamicObjectCollection tripEntryCol = tripReqBill.getDynamicObjectCollection(TripReqBillConstant.TRIPENTRY);
+
+            //行程信息
+            List<TripEntryModel> tripEntryModelList1 = tripReqBillModel.getTripEntryModelList();
+            for (int i = 0; i < tripEntryModelList1.size(); i++) {
+                TripEntryModel tripEntryModel = tripEntryModelList1.get(i);
+                DynamicObject tripEntry = tripEntryCol.addNew();
+                //默认行号 必须要
+                tripEntry.set("seq", i + 1);
+                //出发地
+                tripEntry.set(TripReqBillConstant.FROM, cityMap.get(tripEntryModel.getFrom()));
+                //目的地
+                tripEntry.set(TripReqBillConstant.TO, cityMap.get(tripEntryModel.getTo()));
+                //行程期间
+                tripEntry.set(TripReqBillConstant.STARTDATE, DateUtil.string2date(tripEntryModel.getStartDate(), DateUtil.DATE_FORMAT_YYYY_MM_DD));
+                tripEntry.set(TripReqBillConstant.ENDDATE, DateUtil.string2date(tripEntryModel.getEndDate(), DateUtil.DATE_FORMAT_YYYY_MM_DD));
+                //交通工具
+                tripEntry.set(TripReqBillConstant.VEHICLES, tripEntryModel.getVehicle());
+                //费用承担部门
+                tripEntry.set(TripReqBillConstant.ENTRYCOSTDEPT, orgMap.get(tripEntryModel.getCostDept()));
+                //费用承担公司
+                tripEntry.set(TripReqBillConstant.ENTRYCOSTCOMPANY, orgMap.get(tripEntryModel.getCostCompany()));
+                //成本中心
+                tripEntry.set(TripReqBillConstant.STD_ENTRYCOSTCENTER, costCenterMap.get(tripEntryModel.getCostCenter()));
+                //费用项目
+                tripEntry.set(TripReqBillConstant.TRIPEXPENSEITEM, expenseItem);
+                //申请金额  核定金额(本位币) 核定金额
+                BigDecimal applyAmount = tripEntryModel.getTripOriAmount();
+                tripEntry.set(TripReqBillConstant.TRIPORIAMOUNT, applyAmount);
+                tripEntry.set(TripReqBillConstant.TRIPACCAPPAMOUNT, applyAmount);
+                tripEntry.set(TripReqBillConstant.TRIPORIACCAPPAMOUNT, applyAmount);
+                amountCount = amountCount.add(applyAmount);
+                //行程币种
+                tripEntry.set(TripReqBillConstant.TRIPCURRENCY, currency);
+                //汇率
+                tripEntry.set(TripReqBillConstant.TRIPEXCHANGERATE, 1);
+
+            }
+            //总金额
+            tripReqBill.set(TripReqBillConstant.AMOUNT, amountCount);
+            //核定金额
+            tripReqBill.set(TripReqBillConstant.APPROVEAMOUNT, amountCount);
+
+            addList.add(tripReqBill);
+        }
+        return addList;
+    }
+
+    /**
+     * @param addList 新增的出差申请单
+     * @return 获取新增的结果
+     */
+    private CustomApiResult<Object> getResult(List<DynamicObject> addList) {
+        DynamicObject[] objs = addList.toArray(new DynamicObject[0]);
+        try {
+            //保存至审核
+            OperationUtils.operationDoSave2Audit(objs, TripReqBillConstant.FORMBILLID);
+            return CustomApiResult.success(null);
+        } catch (Exception e) {
+            //未审核成功,找出已存在数据库中的数据,将已存在数据库的数据反审核至删除
+            List<DynamicObject> objList = Arrays.stream(objs).filter(it -> it.getDataEntityState().getFromDatabase()).collect(Collectors.toList());
+            if (!objList.isEmpty()) {
+                OperationUtils.operationDoUnAudit2Delete(objList.toArray(new DynamicObject[0]), TripReqBillConstant.FORMBILLID);
+            }
+            return CustomApiResult.fail("500", e.getMessage());
+        }
+    }
+
+
+}

+ 126 - 0
nckd-fi/src/main/java/nckd/fi/er/webapi/model/TripEntryModel.java

@@ -0,0 +1,126 @@
+package nckd.fi.er.webapi.model;
+
+import kd.bos.openapi.common.custom.annotation.ApiModel;
+import kd.bos.openapi.common.custom.annotation.ApiParam;
+import nckd.base.common.model.BaseModel;
+
+import java.math.BigDecimal;
+
+/**
+ * @description:出差申请.行程信息
+ * @author: dingsixi
+ * @create: 2025/12/11 15:40
+ */
+@ApiModel
+public class TripEntryModel extends BaseModel {
+    @ApiParam(value = "出差人",required = true)
+    private String travelers ;
+
+    @ApiParam(value = "出发地",required = true)
+    private String from ;
+
+    @ApiParam(value = "目的地",required = true)
+    private String to ;
+
+    @ApiParam(value = "行程期间.开始日期",required = true)
+    private String startDate ;
+
+    @ApiParam(value = "行程期间.结束日期",required = true)
+    private String endDate ;
+
+    @ApiParam(value = "交通工具",required = true)
+    private String vehicle ;
+
+    @ApiParam(value = "费用承担公司",required = true)
+    private String costCompany ;
+
+    @ApiParam(value = "费用承担部门",required = true)
+    private String costDept ;
+
+    @ApiParam(value = "成本中心")
+    private String costCenter ;
+
+
+    @ApiParam(value = "申请金额",required = true)
+    private BigDecimal tripOriAmount ;
+
+    public String getTravelers() {
+        return travelers;
+    }
+
+    public void setTravelers(String travelers) {
+        this.travelers = travelers;
+    }
+
+    public String getFrom() {
+        return from;
+    }
+
+    public void setFrom(String from) {
+        this.from = from;
+    }
+
+    public String getTo() {
+        return to;
+    }
+
+    public void setTo(String to) {
+        this.to = to;
+    }
+
+    public String getStartDate() {
+        return startDate;
+    }
+
+    public void setStartDate(String startDate) {
+        this.startDate = startDate;
+    }
+
+    public String getEndDate() {
+        return endDate;
+    }
+
+    public void setEndDate(String endDate) {
+        this.endDate = endDate;
+    }
+
+    public String getVehicle() {
+        return vehicle;
+    }
+
+    public void setVehicle(String vehicle) {
+        this.vehicle = vehicle;
+    }
+
+    public String getCostCompany() {
+        return costCompany;
+    }
+
+    public void setCostCompany(String costCompany) {
+        this.costCompany = costCompany;
+    }
+
+    public String getCostDept() {
+        return costDept;
+    }
+
+    public void setCostDept(String costDept) {
+        this.costDept = costDept;
+    }
+
+    public String getCostCenter() {
+        return costCenter;
+    }
+
+    public void setCostCenter(String costCenter) {
+        this.costCenter = costCenter;
+    }
+
+    public BigDecimal getTripOriAmount() {
+        return tripOriAmount;
+    }
+
+    public void setTripOriAmount(BigDecimal tripOriAmount) {
+        this.tripOriAmount = tripOriAmount;
+    }
+}

+ 114 - 0
nckd-fi/src/main/java/nckd/fi/er/webapi/model/TripReqBillModel.java

@@ -0,0 +1,114 @@
+package nckd.fi.er.webapi.model;
+
+import kd.bos.openapi.common.custom.annotation.ApiModel;
+import kd.bos.openapi.common.custom.annotation.ApiParam;
+import nckd.base.common.model.BaseModel;
+
+import java.util.List;
+
+/**
+ * @description:出差申请
+ * @author: dingsixi
+ * @create: 2025/12/11 14:53
+ */
+@ApiModel
+public class TripReqBillModel extends BaseModel {
+    @ApiParam(value = "申请人",required = true)
+    private String applier;
+
+    @ApiParam(value = "申请日期",required = true)
+    private String applyDate;
+
+    @ApiParam(value = "是否多出差人:多个出差人填true,单个出差人填false")
+    private Boolean isTravelers;
+
+    @ApiParam(value = "出差人:行程信息内填写的出差人",required = true)
+    private List<String> multiTravelers;
+
+    @ApiParam(value = "事由")
+    private String description;
+
+    @ApiParam(value = "费用承担公司",required = true)
+    private String costCompany;
+
+    @ApiParam(value = "费用承担部门",required = true)
+    private String costDept;
+
+    @ApiParam(value = "职位",required = true)
+    private String applierPosition;
+
+    @ApiParam(value = "行程信息",required = true)
+    private List<TripEntryModel> tripEntryModelList;
+
+    public String getApplierPosition() {
+        return applierPosition;
+    }
+
+    public void setApplierPosition(String applierPosition) {
+        this.applierPosition = applierPosition;
+    }
+
+    public String getCostCompany() {
+        return costCompany;
+    }
+
+    public void setCostCompany(String costCompany) {
+        this.costCompany = costCompany;
+    }
+
+    public String getCostDept() {
+        return costDept;
+    }
+
+    public void setCostDept(String costDept) {
+        this.costDept = costDept;
+    }
+
+    public List<String> getMultiTravelers() {
+        return multiTravelers;
+    }
+
+    public void setMultiTravelers(List<String> multiTravelers) {
+        this.multiTravelers = multiTravelers;
+    }
+
+    public String getApplier() {
+        return applier;
+    }
+
+    public void setApplier(String applier) {
+        this.applier = applier;
+    }
+
+    public String getApplyDate() {
+        return applyDate;
+    }
+
+    public void setApplyDate(String applyDate) {
+        this.applyDate = applyDate;
+    }
+
+    public Boolean getTravelers() {
+        return isTravelers;
+    }
+
+    public void setTravelers(Boolean travelers) {
+        isTravelers = travelers;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public List<TripEntryModel> getTripEntryModelList() {
+        return tripEntryModelList;
+    }
+
+    public void setTripEntryModelList(List<TripEntryModel> tripEntryModelList) {
+        this.tripEntryModelList = tripEntryModelList;
+    }
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.