gaoluyang
5 天以前 defe1e1c3ade87de0a7990f565f08f93aeb2decc
fix(bpm): 统一审批标题单号并收紧各模块提交校验

- BPM:创建流程实例时复制变量再过滤,不再修改调用方 Map;默认流程标题优先取 businessNo,显示「流程名-业务单号」
- HRM 离职:修改/删除/提交校验操作人(申请人或创建人),申请人不可变更;提交以申请人身份发起流程,仅审批通过时写审批时间
- HRM 调岗:新增/修改/提交校验调入部门与岗位有效、调出部门与员工档案一致;驳回后可编辑重提;流程定义必须属于调岗审批分类
- MES 采购入库:新增 approve_time 字段并在审批通过时写入;提交审批校验明细非空、流程 key 必填且属于采购入库审批分类;登录用户缺失时明确报错
- MES 生产报工:工单开启报工审批但未配置流程时明确报错;流程必须激活且属于报工审批分类
- MES 发货通知:仅草稿可提交,错误提示写明当前状态;流程必须属于发货审批分类
- 流程变量统一传 businessNo,废料出售同步补充
- SQL:采购入库审批时间字段;销售角色补 bpm:process-instance:cancel 权限

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加2个文件
已修改12个文件
249 ■■■■■ 文件已修改
sql/mysql/20261107_item_receipt_approve_time.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261108_hrm_sales_process_cancel_permission.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptRespVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemreceipt/MesWmItemReceiptDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/scrap/MesWmScrapSaleService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261107_item_receipt_approve_time.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- é‡‡è´­å…¥åº“审批通过时间:用于业务单据详情与导出。
ALTER TABLE mes_wm_item_receipt
    ADD COLUMN approve_time DATETIME NULL COMMENT '审批通过时间' AFTER process_instance_id;
sql/mysql/20261108_hrm_sales_process_cancel_permission.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
-- é”€å”®è§’色的人事申请人可在“我的流程”撤销本人发起的实例。
-- BPM æ’¤é”€æŽ¥å£ä»æŒ‰æµç¨‹å‘起人校验,不能撤销他人的实例。
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted, tenant_id)
SELECT 165, 1220, '1', NOW(), '1', NOW(), b'0', 0
WHERE EXISTS (SELECT 1 FROM system_role WHERE id = 165 AND deleted = b'0')
  AND EXISTS (SELECT 1 FROM system_menu WHERE id = 1220 AND permission = 'bpm:process-instance:cancel' AND deleted = b'0')
  AND NOT EXISTS (SELECT 1 FROM system_role_menu WHERE role_id = 165 AND menu_id = 1220 AND deleted = b'0');
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java
@@ -9,6 +9,9 @@
 */
public class BpmnVariableConstants {
    /** ä¸šåŠ¡æµç¨‹çš„å¯è¯»å•å·ï¼Œç”¨äºŽå¾…åŠžæµç¨‹æ ‡é¢˜ã€‚ */
    public static final String PROCESS_INSTANCE_VARIABLE_BUSINESS_NO = "businessNo";
    /**
     * æµç¨‹å®žä¾‹çš„变量 - çŠ¶æ€
     *
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
@@ -175,8 +175,9 @@
     * @return è¿‡æ»¤åŽçš„表单
     */
    public static Map<String, Object> filterProcessInstanceFormVariable(Map<String, Object> processVariables) {
        processVariables.remove(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
        return processVariables;
        Map<String, Object> filteredVariables = new HashMap<>(processVariables);
        filteredVariables.remove(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
        return filteredVariables;
    }
    /**
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -5,6 +5,7 @@
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
@@ -844,10 +845,8 @@
        validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables);
        // 2. åˆ›å»ºæµç¨‹å®žä¾‹
        if (variables == null) {
            variables = new HashMap<>();
        }
        FlowableUtils.filterProcessInstanceFormVariable(variables); // è¿‡æ»¤ä¸€ä¸‹ï¼Œé¿å… ProcessInstance ç³»ç»Ÿçº§çš„变量被占用
        variables = FlowableUtils.filterProcessInstanceFormVariable(
                variables == null ? new HashMap<>() : variables); // å¤åˆ¶å¹¶è¿‡æ»¤ï¼Œé¿å…ä¿®æ”¹è°ƒç”¨æ–¹çš„变量
        variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // è®¾ç½®æµç¨‹å˜é‡ï¼Œå‘起人 ID
        variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // æµç¨‹å®žä¾‹çŠ¶æ€ï¼šå®¡æ‰¹ä¸­
                BpmProcessInstanceStatusEnum.RUNNING.getStatus());
@@ -914,6 +913,11 @@
        }
        BpmModelMetaInfoVO.TitleSetting titleSetting = definitionInfo.getTitleSetting();
        if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) {
            String businessNo = MapUtil.getStr(variables,
                    BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_BUSINESS_NO);
            if (StrUtil.isNotBlank(businessNo)) {
                return definition.getName() + "-" + businessNo;
            }
            return definition.getName();
        }
        AdminUserRespDTO user = adminUserApi.getUser(userId);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.hrm.service.approval;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
@@ -150,8 +151,12 @@
    @Override
    public void updateResignationApplication(HrmResignationApplicationSaveReqVO updateReqVO) {
        HrmResignationApplicationDO application = validateResignationApplicationExists(updateReqVO.getId());
        validateOperator(application, getCurrentUserId());
        if (!isEditableResignationStatus(application.getStatus())) {
            throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        if (updateReqVO.getUserId() != null && !Objects.equals(updateReqVO.getUserId(), application.getUserId())) {
            throw exception(RESIGNATION_APPLICATION_APPLICANT_IMMUTABLE);
        }
        HrmResignationApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmResignationApplicationDO.class);
        // ä¸è¦†ç›–申请人;交接人统一解析成“员工ID + ç”¨æˆ·ID”
@@ -177,6 +182,7 @@
    @Override
    public void deleteResignationApplication(Long id) {
        HrmResignationApplicationDO application = validateResignationApplicationExists(id);
        validateOperator(application, getCurrentUserId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
@@ -328,6 +334,7 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitResignationApplication(Long id, String processDefinitionKey, Long userId) {
        HrmResignationApplicationDO application = validateResignationApplicationExists(id);
        validateOperator(application, userId);
        if (!isEditableResignationStatus(application.getStatus())) {
            throw exception(RESIGNATION_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
        }
@@ -339,9 +346,17 @@
        }
        ProcessDefinition processDefinition = resolveActiveProcessDefinition(processDefinitionKey);
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinition.getKey()).setBusinessKey(String.valueOf(id)));
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(application.getUserId(), new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinition.getKey()).setBusinessKey(String.valueOf(id))
                .setVariables(Map.of("businessNo", application.getNo())));
        resignationApplicationMapper.startApproval(id, processInstanceId, HrmAuditStatusEnum.PROCESS.getStatus());
    }
    private void validateOperator(HrmResignationApplicationDO application, Long operatorId) {
        if (!Objects.equals(application.getUserId(), operatorId)
                && !Objects.equals(application.getCreator(), String.valueOf(operatorId))) {
            throw exception(RESIGNATION_APPLICATION_OPERATOR_NOT_ALLOWED);
        }
    }
    private void validateApproveCategoryAndProcessDefinition() {
@@ -436,11 +451,12 @@
            log.error("[updateResignationApplicationAuditStatus][application({}) ä¸å¤„于审批中,无法更新审批结果({})]", id, status);
            throw exception(RESIGNATION_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder()
                .id(id)
                .status(status)
                .approveTime(java.time.LocalDateTime.now())
                .build());
        resignationApplicationMapper.update(null, new LambdaUpdateWrapper<HrmResignationApplicationDO>()
                .eq(HrmResignationApplicationDO::getId, id)
                .set(HrmResignationApplicationDO::getStatus, status)
                .set(HrmResignationApplicationDO::getApproveTime,
                        Objects.equals(status, HrmAuditStatusEnum.APPROVE.getStatus())
                                ? LocalDateTime.now() : null));
    }
    @Override
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -15,7 +16,9 @@
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmTransferApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmTransferApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmTransferTypeEnum;
@@ -60,6 +63,8 @@
    @Resource
    private HrmTransferApplicationMapper transferApplicationMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private HrmNoRedisDAO noRedisDAO;
    @Resource
    private DeptApi deptApi;
@@ -87,6 +92,7 @@
        if (Objects.equals(createReqVO.getTargetPostId(), createReqVO.getCurrentPostId())) {
            throw exception(TRANSFER_APPLICATION_SAME_POST);
        }
        validateTargetDeptAndPost(createReqVO.getTargetDeptId(), createReqVO.getTargetPostId());
        // 3. ç¡®å®šè¢«è°ƒå²—员工(未指定时默认当前登录用户)
        Long userId = createReqVO.getUserId() != null ? createReqVO.getUserId() : getLoginUserId();
        AdminUserRespDTO user = userId != null ? adminUserApi.getUser(userId) : null;
@@ -99,6 +105,7 @@
            throw new ServiceException(TRANSFER_APPLICATION_APPLICANT_DEPT_EMPTY.getCode(),
                    String.format("调岗申请失败:员工[%s]未设置部门,无法确定原部门。请先到「员工管理」编辑该员工档案补全部门,再重新发起调岗申请。", user.getNickname()));
        }
        validateCurrentDept(userId, createReqVO.getCurrentDeptId());
        if (createReqVO.getCurrentPostId() == null) {
            throw new ServiceException(TRANSFER_APPLICATION_APPLICANT_POST_EMPTY.getCode(),
                    String.format("调岗申请失败:员工[%s]未设置岗位,无法确定原岗位。请先到「员工管理」编辑该员工档案补全岗位,再重新发起调岗申请。", user.getNickname()));
@@ -131,9 +138,13 @@
    @Override
    public void updateTransferApplication(HrmTransferApplicationSaveReqVO updateReqVO) {
        HrmTransferApplicationDO application = validateTransferApplicationExists(updateReqVO.getId());
        validateOperator(application, getLoginUserId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_EDITABLE);
        }
        if (updateReqVO.getUserId() != null && !Objects.equals(updateReqVO.getUserId(), application.getUserId())) {
            throw exception(TRANSFER_APPLICATION_APPLICANT_IMMUTABLE);
        }
        if (Objects.equals(updateReqVO.getTargetDeptId(), updateReqVO.getCurrentDeptId())) {
            throw exception(TRANSFER_APPLICATION_SAME_DEPT);
@@ -141,7 +152,11 @@
        if (Objects.equals(updateReqVO.getTargetPostId(), updateReqVO.getCurrentPostId())) {
            throw exception(TRANSFER_APPLICATION_SAME_POST);
        }
        validateTargetDeptAndPost(updateReqVO.getTargetDeptId(), updateReqVO.getTargetPostId());
        validateCurrentDept(application.getUserId(),
                updateReqVO.getCurrentDeptId() != null ? updateReqVO.getCurrentDeptId() : application.getCurrentDeptId());
        HrmTransferApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmTransferApplicationDO.class);
        updateObj.setUserId(null);
        transferApplicationMapper.updateById(updateObj);
        storageAttachmentApi.updateAttachments("file", "hrm_transfer_application", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
@@ -149,6 +164,7 @@
    @Override
    public void deleteTransferApplication(Long id) {
        HrmTransferApplicationDO application = validateTransferApplicationExists(id);
        validateOperator(application, getLoginUserId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
@@ -263,9 +279,13 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitTransferApplication(Long id, String processDefinitionKey, Long userId) {
        HrmTransferApplicationDO application = validateTransferApplicationExists(id);
        if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
        validateOperator(application, userId);
        if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                && ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
            throw exception(TRANSFER_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
        }
        validateTargetDeptAndPost(application.getTargetDeptId(), application.getTargetPostId());
        validateCurrentDept(application.getUserId(), application.getCurrentDeptId());
        // å…¨å±€å…å®¡æ‰¹ï¼šä¸šåŠ¡æ ¡éªŒé€šè¿‡åŽç›´æŽ¥æ‰§è¡Œå®¡æ‰¹é€šè¿‡é€»è¾‘ï¼Œæ— éœ€æµç¨‹æ¨¡åž‹ã€‚
        if (bpmProcessInstanceApi.isGlobalAutoApprove()) {
            transferApplicationMapper.updateById(HrmTransferApplicationDO.builder()
@@ -278,17 +298,45 @@
        }
        validateApproveCategoryAndProcessDefinition();
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null) {
        if (processDefinitionKey == null || processDefinitionKey.isBlank()) {
            throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
        transferApplicationMapper.updateById(HrmTransferApplicationDO.builder()
                .id(id)
                .processInstanceId(processInstanceId)
                .status(HrmAuditStatusEnum.PROCESS.getStatus())
                .build());
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null || bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE).stream()
                .noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) {
            throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(application.getUserId(), new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id))
                .setVariables(Map.of("businessNo", application.getNo())));
        transferApplicationMapper.update(null, new LambdaUpdateWrapper<HrmTransferApplicationDO>()
                .eq(HrmTransferApplicationDO::getId, id)
                .set(HrmTransferApplicationDO::getProcessInstanceId, processInstanceId)
                .set(HrmTransferApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
                .set(HrmTransferApplicationDO::getApproveTime, null));
    }
    private void validateTargetDeptAndPost(Long targetDeptId, Long targetPostId) {
        deptApi.validateDeptList(Collections.singletonList(targetDeptId));
        postApi.validPostList(Collections.singletonList(targetPostId));
    }
    private void validateOperator(HrmTransferApplicationDO application, Long operatorId) {
        if (!Objects.equals(application.getUserId(), operatorId)
                && !Objects.equals(application.getCreator(), String.valueOf(operatorId))) {
            throw exception(TRANSFER_APPLICATION_OPERATOR_NOT_ALLOWED);
        }
    }
    private void validateCurrentDept(Long userId, Long currentDeptId) {
        HrmEmployeeDO employee = employeeMapper.selectByUserId(userId);
        if (employee == null || employee.getDeptId() == null) {
            throw exception(TRANSFER_APPLICATION_EMPLOYEE_DEPT_NOT_FOUND);
        }
        if (!Objects.equals(employee.getDeptId(), currentDeptId)) {
            throw exception(TRANSFER_APPLICATION_CURRENT_DEPT_MISMATCH);
        }
    }
    private void validateApproveCategoryAndProcessDefinition() {
@@ -344,11 +392,12 @@
            log.error("[updateTransferApplicationAuditStatus][application({}) ä¸å¤„于审批中,无法更新审批结果({})]", id, status);
            throw exception(TRANSFER_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        transferApplicationMapper.updateById(HrmTransferApplicationDO.builder()
                .id(id)
                .status(status)
                .approveTime(java.time.LocalDateTime.now())
                .build());
        transferApplicationMapper.update(null, new LambdaUpdateWrapper<HrmTransferApplicationDO>()
                .eq(HrmTransferApplicationDO::getId, id)
                .set(HrmTransferApplicationDO::getStatus, status)
                .set(HrmTransferApplicationDO::getApproveTime,
                        Objects.equals(status, HrmAuditStatusEnum.APPROVE.getStatus())
                                ? java.time.LocalDateTime.now() : null));
    }
    @Override
@@ -356,4 +405,4 @@
        return transferApplicationMapper.selectByProcessInstanceId(processInstanceId);
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptRespVO.java
@@ -60,6 +60,10 @@
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号", example = "xxx")
    private String processInstanceId;
    @Schema(description = "审批通过时间")
    @ExcelProperty("审批通过时间")
    private LocalDateTime approveTime;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemreceipt/MesWmItemReceiptDO.java
@@ -88,6 +88,8 @@
     * BPM æµç¨‹å®šä¹‰ Key
     */
    private String processDefinitionKey;
    /** å®¡æ‰¹é€šè¿‡æ—¶é—´ï¼›é©³å›žæˆ–撤销时为空。 */
    private LocalDateTime approveTime;
    /**
     * å¤‡æ³¨
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -354,6 +354,10 @@
            "工序「{}」按计时计薪,请填写本次报工工时(小时),工时必须大于 0;若实际按数量计薪,请先把该工序的计费类型改为计件");
    ErrorCode PRO_FEEDBACK_TIME_BILLING_QUICK_UNSUPPORTED = new ErrorCode(1_040_506_020,
            "工序「{}」按计时计薪,快捷报工无法录入工时(小时);请到「生产报工」页面新增报工并填写工时");
    ErrorCode PRO_FEEDBACK_APPROVAL_PROCESS_KEY_REQUIRED = new ErrorCode(1_040_506_021,
            "工单已开启报工审批,但未配置审批流程,请先为工单配置流程后再提交报工");
    ErrorCode PRO_FEEDBACK_APPROVAL_PROCESS_KEY_INVALID = new ErrorCode(1_040_506_022,
            "工单配置的报工审批流程未激活或不属于报工审批分类,请检查工单审批配置");
    // ========== MES ç”Ÿäº§ç®¡ç†-辅助工时配置(1-040-510-000) ==========
    ErrorCode PRO_WORKTIME_AUX_NOT_EXISTS = new ErrorCode(1_040_510_000, "辅助工时项目不存在");
@@ -590,6 +594,7 @@
    ErrorCode WM_ITEM_RECEIPT_STATUS_ERROR = new ErrorCode(1_040_705_005, "入库单状态不正确");
    ErrorCode WM_ITEM_RECEIPT_CANCEL_NOT_ALLOWED = new ErrorCode(1_040_705_006, "已完成或已取消的入库单不允许取消");
    ErrorCode WM_ITEM_RECEIPT_ALREADY_EXISTS = new ErrorCode(1_040_705_007, "该到货通知单已生成入库单,不能重复生成");
    ErrorCode WM_ITEM_RECEIPT_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_040_705_008, "采购入库审批流程未配置或所选流程不属于采购入库审批");
    ErrorCode WM_PURCHASE_ORDER_NOT_EXISTS = new ErrorCode(1_040_705_008, "采购订单不存在");
    ErrorCode WM_PURCHASE_ORDER_NO_ITEM = new ErrorCode(1_040_705_008, "采购订单无明细数据");
    ErrorCode WM_ITEM_RECEIPT_LINE_NOT_EXISTS = new ErrorCode(1_040_705_100, "采购入库单行不存在");
@@ -854,6 +859,7 @@
    ErrorCode WM_SALES_NOTICE_SUBMIT_PROCESS_KEY_REQUIRED = new ErrorCode(1_040_720_020, "提交发货审批前必须选择审批流程:未传流程定义 Key");
    ErrorCode WM_SALES_NOTICE_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_040_720_021, "发货审批流程未配置:请先在「流程管理」中设计并启用「发货审批」流程,再提交发货单");
    ErrorCode WM_SALES_NOTICE_UPDATE_AUDIT_STATUS_FAIL_NOT_APPROVING = new ErrorCode(1_040_720_022, "发货单({})当前状态为[{}],不处于审批中,无法更新审批结果");
    ErrorCode WM_SALES_NOTICE_STATUS_NOT_ALLOW_SUBMIT = new ErrorCode(1_040_720_023, "只有草稿状态的发货通知单才能提交审批");
    // ========== MES ä»“库管理-条码配置(1-040-730-000) ==========
    ErrorCode WM_BARCODE_CONFIG_NOT_EXISTS = new ErrorCode(1_040_730_000, "条码配置不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -4,11 +4,14 @@
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import org.flowable.engine.repository.ProcessDefinition;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
@@ -76,6 +79,7 @@
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -90,6 +94,8 @@
@Validated
@lombok.extern.slf4j.Slf4j
public class MesProFeedbackServiceImpl implements MesProFeedbackService {
    private static final String FEEDBACK_APPROVE_CATEGORY_CODE = "mes_pro_feedback_audit";
    @Resource
    private MesProFeedbackEquipmentService equipmentService;
@@ -147,6 +153,8 @@
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
    private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -267,16 +275,28 @@
        }
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
        if (workOrder != null && Boolean.TRUE.equals(workOrder.getFeedbackApprove())
                && StrUtil.isBlank(workOrder.getProcessDefinitionKey())) {
            throw exception(PRO_FEEDBACK_APPROVAL_PROCESS_KEY_REQUIRED);
        }
        // 1.2 æŠ¥å·¥ä¸Žè´¨æ£€åˆ†ç¦»ï¼šæäº¤æŠ¥å·¥ä¸å†è‡ªåŠ¨ç”Ÿæˆè¿‡ç¨‹æ£€éªŒå•ï¼Œè´¨æ£€ç”±è´¨æ£€éƒ¨é—¨æ‰‹å·¥å»ºå•
        if (!processInstanceApi.isGlobalAutoApprove() && workOrder != null
                && Boolean.TRUE.equals(workOrder.getFeedbackApprove())
                && cn.hutool.core.util.StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) {
                && StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) {
            ProcessDefinition processDefinition = bpmProcessDefinitionService
                    .getActiveProcessDefinition(workOrder.getProcessDefinitionKey());
            if (processDefinition == null || bpmProcessDefinitionService
                    .getProcessDefinitionInfoListByCategory(FEEDBACK_APPROVE_CATEGORY_CODE).stream()
                    .noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) {
                throw exception(PRO_FEEDBACK_APPROVAL_PROCESS_KEY_INVALID);
            }
            // 2. æ›´æ–°çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­ï¼Œå¹¶å¯åŠ¨æµç¨‹
            String processInstanceId = processInstanceApi.createProcessInstance(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(),
                    new cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO()
                            .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
                            .setBusinessKey(String.valueOf(id)));
                            .setBusinessKey(String.valueOf(id))
                            .setVariables(Map.of("businessNo", feedback.getCode())));
            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                    .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
@@ -292,8 +312,11 @@
            }
            // 3. æ›´æ–°çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­ï¼Œç„¶åŽç›´æŽ¥é€šè¿‡å®¡æ‰¹
            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                    .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
            feedbackMapper.update(null, new LambdaUpdateWrapper<MesProFeedbackDO>()
                    .eq(MesProFeedbackDO::getId, id)
                    .set(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.APPROVING.getStatus())
                    .set(MesProFeedbackDO::getProcessInstanceId, null)
                    .set(MesProFeedbackDO::getProcessDefinitionKey, null));
            MesProFeedbackApproveReqVO approveReqVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(reqVO, MesProFeedbackApproveReqVO.class);
            // è‡ªåŠ¨å®¡æ‰¹ï¼šè¿‡ç¨‹æ£€éªŒå•åˆšç”Ÿæˆå°šæœªå¡«å†™ï¼Œä¸åšè´¨æ£€å·¥èµ„å‰ç½®æ ¡éªŒï¼ˆè´¨é‡è°ƒæ•´ç•™ç©ºï¼‰
            approveFeedback(approveReqVO, false);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java
@@ -2,6 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -25,26 +27,29 @@
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.flowable.engine.repository.ProcessDefinition;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -54,6 +59,8 @@
@Validated
@Slf4j
public class MesWmItemReceiptServiceImpl implements MesWmItemReceiptService {
    private static final String ITEM_RECEIPT_APPROVE_CATEGORY_CODE = "item_receipt_approve";
    @Resource
    private MesWmItemReceiptMapper itemReceiptMapper;
@@ -79,6 +86,8 @@
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
@@ -465,7 +474,12 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitItemReceiptApproval(Long id, String processDefinitionKey) {
        // 1. æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateItemReceiptExistsAndDraft(id);
        MesWmItemReceiptDO receipt = validateItemReceiptExistsAndDraft(id);
        // å®¡æ‰¹å…¥å£ä¸Žæ™®é€šæäº¤å…¥å£ä¿æŒä¸€è‡´ï¼Œç©ºè¡Œå•据不能进入审批。
        List<MesWmItemReceiptLineDO> lines = itemReceiptLineService.getItemReceiptLineListByReceiptId(id);
        if (CollUtil.isEmpty(lines)) {
            throw exception(WM_ITEM_RECEIPT_NO_LINE);
        }
        // 2. åˆ›å»º BPM æµç¨‹å®žä¾‹
        Long userId = getCurrentUserId();
@@ -479,10 +493,21 @@
            return;
        }
        if (StrUtil.isBlank(processDefinitionKey)) {
            throw exception(WM_ITEM_RECEIPT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null || bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(ITEM_RECEIPT_APPROVE_CATEGORY_CODE).stream()
                .noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) {
            throw exception(WM_ITEM_RECEIPT_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = processInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
                        .setBusinessKey(String.valueOf(id))
                        .setVariables(Map.of("businessNo", receipt.getCode())));
        // 3. æ›´æ–°å…¥åº“单状态为审批中
        itemReceiptMapper.updateById(new MesWmItemReceiptDO()
@@ -504,8 +529,11 @@
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToReceiptStatus(bpmResult);
        if (newStatus != null) {
            itemReceiptMapper.updateById(new MesWmItemReceiptDO()
                    .setId(id).setStatus(newStatus));
            itemReceiptMapper.update(null, new LambdaUpdateWrapper<MesWmItemReceiptDO>()
                    .eq(MesWmItemReceiptDO::getId, id)
                    .set(MesWmItemReceiptDO::getStatus, newStatus)
                    .set(MesWmItemReceiptDO::getApproveTime,
                            BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult) ? LocalDateTime.now() : null));
        }
    }
@@ -527,11 +555,7 @@
     * èŽ·å–å½“å‰ç”¨æˆ· ID
     */
    private Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof Long) {
            return (Long) authentication.getPrincipal();
        }
        return 1L; // é»˜è®¤ç”¨æˆ·
        return Objects.requireNonNull(getLoginUserId(), "采购入库审批需要登录用户");
    }
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java
@@ -44,6 +44,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -142,7 +143,10 @@
    @Transactional(rollbackFor = Exception.class)
    public void submitSalesNotice(Long id, String processDefinitionKey, Long userId) {
        // 1.1 æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        MesWmSalesNoticeDO notice = validateSalesNoticeExistsAndDraft(id);
        MesWmSalesNoticeDO notice = validateSalesNoticeExists(id);
        if (ObjUtil.notEqual(PREPARE.getStatus(), notice.getStatus())) {
            throw exception(WM_SALES_NOTICE_STATUS_NOT_ALLOW_SUBMIT);
        }
        // 1.2 æ£€æŸ¥æ˜¯å¦æœ‰è¡Œé¡¹ç›®
        List<MesWmSalesNoticeLineDO> lines = salesNoticeLineService.getSalesNoticeLineListByNoticeId(id);
        if (CollUtil.isEmpty(lines)) {
@@ -163,13 +167,16 @@
        }
        validateSalesNoticeApproveCategoryAndProcessDefinition();
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null) {
        if (processDefinition == null || bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(SALES_DELIVERY_APPROVE_CATEGORY_CODE).stream()
                .noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) {
            throw exception(WM_SALES_NOTICE_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        // 4. åˆ›å»ºå®¡æ‰¹æµç¨‹å®žä¾‹ï¼Œè¿›å…¥å®¡æ‰¹ä¸­
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id))
                .setVariables(Map.of("businessNo", notice.getCode())));
        salesNoticeMapper.updateById(new MesWmSalesNoticeDO()
                .setId(id).setProcessInstanceId(processInstanceId).setStatus(APPROVING.getStatus()));
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/scrap/MesWmScrapSaleService.java
@@ -46,7 +46,7 @@
        saleMapper.updateById(update);
        String processId = processApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(PROCESS_KEY).setBusinessKey(String.valueOf(id))
                .setVariables(Map.of("saleCode", sale.getCode(), "buyer", sale.getBuyer(),
                .setVariables(Map.of("businessNo", sale.getCode(), "saleCode", sale.getCode(), "buyer", sale.getBuyer(),
                        "totalAmount", lines.stream().map(line -> line.getQuantity().multiply(line.getUnitPrice()))
                                .reduce(BigDecimal.ZERO, BigDecimal::add))));
        MesWmScrapSaleDO processUpdate = new MesWmScrapSaleDO();