gaoluyang
5 天以前 8168aef3c7557c9e1361892270a1ebe35dbddba5
refactor(erp): 采购申请与询价比价流程重构

- 采购申请支持由询价比价结果生成订单,新增 ErpPurchaseRequestApi 供跨模块调用
- 询价单增加审批流程与待处理列表相关字段
- 调整采购菜单排序,补充流程配置 SQL

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加6个文件
已修改22个文件
1073 ■■■■■ 文件已修改
sql/mysql/20261109_purchase_request_inquiry_refactor.sql 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261110_purchase_menu_sort.sql 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261110_srm_inquiry_approve_flow.sql 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseRequestApi.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseOrderCreateReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseRequestRespDTO.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseRequestApiImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderSourceTypeEnum.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/inquiry/SrmInquiryController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/inquiry/vo/SrmInquiryRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/dataobject/SrmInquiryDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/inquiry/SrmInquiryService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/inquiry/SrmInquiryServiceImpl.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/20261109_purchase_request_inquiry_refactor.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,123 @@
-- =============================================
-- é‡‡è´­ç”³è¯·åŽ»ä»·æ ¼ + è¯¢ä»·æ¯”ä»· / é‡‡è´­å°è´¦è¡”接改造
-- ç›®æ ‡åº“:mom-ntsh(MySQL 8.4)
-- è¯´æ˜Žï¼š
--   1. é‡‡è´­ç”³è¯·ï¼ˆè¡¨å¤´ + æ˜Žç»†ï¼‰ä¸å†ä¿å­˜ä»»ä½•价格字段,价格在询价或采购台账阶段确定
--   2. é‡‡è´­ç”³è¯·å®¡æ‰¹é€šè¿‡åŽï¼Œç”±é‡‡è´­å‘˜å†³å®šã€Œå‘起询价」或「直接生成采购台账」
--   3. è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼šåˆ åˆ—/加列均先用 information_schema é¢„检查
--   4. ä¸åŒ…含 tenant_id(本项目不使用多租户)
-- =============================================
-- ---------------------------------------------
-- ä¸€ã€åˆ é™¤é‡‡è´­ç”³è¯·è¡¨å¤´ä»·æ ¼å­—段
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'total_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request DROP COLUMN total_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'total_product_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request DROP COLUMN total_product_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'total_tax_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request DROP COLUMN total_tax_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'discount_percent');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request DROP COLUMN discount_percent', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'discount_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request DROP COLUMN discount_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'deposit_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request DROP COLUMN deposit_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- äºŒã€åˆ é™¤é‡‡è´­ç”³è¯·æ˜Žç»†ä»·æ ¼å­—段
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request_items' AND column_name = 'product_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request_items DROP COLUMN product_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request_items' AND column_name = 'total_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request_items DROP COLUMN total_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request_items' AND column_name = 'tax_percent');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request_items DROP COLUMN tax_percent', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request_items' AND column_name = 'tax_price');
SET @sql = IF(@exists > 0, 'ALTER TABLE erp_purchase_request_items DROP COLUMN tax_price', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- ä¸‰ã€é‡‡è´­ç”³è¯·å…³è”询价单(采购员审批通过后发起询价时回写)
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'inquiry_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_request ADD COLUMN inquiry_id BIGINT NULL COMMENT ''关联询价单编号'' AFTER order_no',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_request' AND column_name = 'inquiry_no');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_request ADD COLUMN inquiry_no VARCHAR(64) NULL COMMENT ''关联询价单号'' AFTER inquiry_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- å››ã€è¯¢ä»·å•关联采购申请
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'srm_inquiry' AND column_name = 'purchase_request_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE srm_inquiry ADD COLUMN purchase_request_id BIGINT NULL COMMENT ''关联采购申请编号'' AFTER process_instance_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'srm_inquiry' AND column_name = 'purchase_request_no');
SET @sql = IF(@exists = 0,
    'ALTER TABLE srm_inquiry ADD COLUMN purchase_request_no VARCHAR(64) NULL COMMENT ''关联采购申请单号'' AFTER purchase_request_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------
-- äº”、采购台账记录来源采购申请(一张申请可能因多供应商生成多张台账)
-- ---------------------------------------------
SET @exists = (SELECT COUNT(*) FROM information_schema.columns
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND column_name = 'purchase_request_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD COLUMN purchase_request_id BIGINT NULL COMMENT ''来源采购申请编号'' AFTER source_id',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.statistics
               WHERE table_schema = DATABASE() AND table_name = 'erp_purchase_order' AND index_name = 'idx_purchase_request_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE erp_purchase_order ADD INDEX idx_purchase_request_id (purchase_request_id)',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exists = (SELECT COUNT(*) FROM information_schema.statistics
               WHERE table_schema = DATABASE() AND table_name = 'srm_inquiry' AND index_name = 'idx_purchase_request_id');
SET @sql = IF(@exists = 0,
    'ALTER TABLE srm_inquiry ADD INDEX idx_purchase_request_id (purchase_request_id)',
    'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
sql/mysql/20261110_purchase_menu_sort.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
-- =============================================================================
-- é‡‡è´­ç®¡ç†èœå•按业务流转顺序重排
-- ç›®æ ‡åº“:mom-ntsh
-- é¡ºåºï¼šé‡‡è´­ç”³è¯· â†’ è¯¢ä»·æ¯”ä»· â†’ é‡‡è´­å°è´¦ â†’ é‡‡è´­æ¥ç¥¨ â†’ ä»˜æ¬¾ â†’ ä¾›åº”商往来 â†’ ä¾›åº”商档案
-- è¯´æ˜Žï¼šä¾›åº”商档案属于基础档案,放在业务单据之后。
-- =============================================================================
UPDATE system_menu
   SET sort = CASE id
                WHEN 602498  THEN 1  -- é‡‡è´­ç”³è¯·
                WHEN 602620  THEN 2  -- è¯¢ä»·æ¯”ä»·
                WHEN 2666    THEN 3  -- é‡‡è´­å°è´¦
                WHEN 1063110 THEN 4  -- é‡‡è´­æ¥ç¥¨
                WHEN 1063116 THEN 5  -- ä»˜æ¬¾
                WHEN 602640  THEN 6  -- ä¾›åº”商往来
                WHEN 602525  THEN 7  -- ä¾›åº”商档案
              END
 WHERE id IN (602498, 602620, 2666, 1063110, 1063116, 602640, 602525)
   AND deleted = 0;
SELECT id, name, sort
  FROM system_menu
 WHERE parent_id = 2602 AND deleted = 0
 ORDER BY sort, id;
sql/mysql/20261110_srm_inquiry_approve_flow.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
-- =============================================================================
-- è¡¥éƒ¨ç½² SRM è¯¢ä»·å•审批流程:srm_inquiry_approve
-- ç›®æ ‡åº“:mom-ntsh
-- èƒŒæ™¯ï¼šSrmInquiryServiceImpl æäº¤å®¡æ ¸æ—¶ä½¿ç”¨æµç¨‹ key=srm_inquiry_approve,
--       ä½† ACT_RE_PROCDEF ä¸­ä¸å­˜åœ¨è¯¥å®šä¹‰ï¼Œå‰ç«¯æäº¤æŠ¥ã€Œæµç¨‹å®šä¹‰ä¸å­˜åœ¨ã€ã€‚
-- æ–¹å¼ï¼šå¤åˆ¶é‡‡è´­ç”³è¯·å®¡æ‰¹ï¼ˆpurchase_request_approve,发起人 + è§’色1审批)模板,
--       ä»…替换 key ä¸Žæ˜¾ç¤ºåï¼Œä¿æŒä¸Žé‡‡è´­å•据审批一致的审批人配置。
-- å¹‚等:key å·²å­˜åœ¨åˆ™è·³è¿‡ï¼Œå¯é‡å¤æ‰§è¡Œã€‚
-- =============================================================================
SET NAMES utf8mb4;
-- æµç¨‹åˆ†ç±»ï¼ˆå¹‚等)
INSERT INTO bpm_category (name, code, description, status, sort, creator, updater, deleted)
SELECT '询价单审批', 'srm_inquiry_approve', '询价单审批', 0, 23, '1', '1', b'0'
 WHERE NOT EXISTS (SELECT 1 FROM bpm_category WHERE code = 'srm_inquiry_approve' AND deleted = b'0');
-- å®¡æ‰¹è¯¦æƒ…业务组件(幂等):让审批详情页渲染询价明细而非空白通用表单
INSERT INTO bpm_business_attach
    (process_definition_key, business_type, component_path, status, remark, creator, updater, deleted)
SELECT 'srm_inquiry_approve', 'srm_inquiry', '/srm/inquiry/detail/approve/index.vue', 0,
       '询价单审批业务详情组件', '1', '1', b'0'
 WHERE NOT EXISTS (SELECT 1 FROM bpm_business_attach
                    WHERE process_definition_key = 'srm_inquiry_approve' AND deleted = b'0');
DROP PROCEDURE IF EXISTS bpm_deploy_srm_inquiry_approve;
DELIMITER $$
CREATE PROCEDURE bpm_deploy_srm_inquiry_approve()
label_deploy: BEGIN
    DECLARE v_p_key      VARCHAR(64)  DEFAULT 'srm_inquiry_approve';
    DECLARE v_p_name     VARCHAR(128) DEFAULT '询价单审批';
    DECLARE v_p_category VARCHAR(64)  DEFAULT 'srm_inquiry_approve';
    DECLARE v_tpl_key    VARCHAR(64)  DEFAULT 'purchase_request_approve';
    DECLARE v_exist      INT DEFAULT 0;
    DECLARE v_tpl_dep    VARCHAR(64) DEFAULT NULL;
    DECLARE v_tpl_name   VARCHAR(255) DEFAULT NULL;
    DECLARE v_dep        VARCHAR(64);
    DECLARE v_model_id   VARCHAR(64);
    DECLARE v_byte_id    VARCHAR(64);
    DECLARE v_procdef_id VARCHAR(64);
    DECLARE v_meta       TEXT;
    DECLARE v_sort       BIGINT;
    DECLARE v_now        DATETIME;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    SELECT COUNT(*) INTO v_exist FROM ACT_RE_PROCDEF WHERE KEY_ = v_p_key;
    IF v_exist > 0 THEN
        SELECT CONCAT('[SKIP] æµç¨‹å·²å­˜åœ¨ï¼Œè·³è¿‡ï¼š', v_p_key) AS deploy_result;
        LEAVE label_deploy;
    END IF;
    SELECT DEPLOYMENT_ID_ INTO v_tpl_dep FROM ACT_RE_PROCDEF
     WHERE KEY_ = v_tpl_key ORDER BY VERSION_ DESC LIMIT 1;
    SELECT NAME_        INTO v_tpl_name FROM ACT_RE_PROCDEF
     WHERE KEY_ = v_tpl_key ORDER BY VERSION_ DESC LIMIT 1;
    IF v_tpl_dep IS NULL THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '模板流程 purchase_request_approve ä¸å­˜åœ¨ï¼Œæ— æ³•复制';
    END IF;
    SET v_now     = NOW(3);
    SET v_sort    = UNIX_TIMESTAMP(v_now) * 1000;
    SET v_dep     = REPLACE(UUID(), '-', '');
    SET v_model_id = REPLACE(UUID(), '-', '');
    SET v_byte_id  = REPLACE(UUID(), '-', '');
    SET v_procdef_id = CONCAT(v_p_key, ':1:', v_dep);
    START TRANSACTION;
    -- 1) éƒ¨ç½²è®°å½•
    INSERT INTO ACT_RE_DEPLOYMENT
        (ID_, NAME_, CATEGORY_, KEY_, TENANT_ID_, DEPLOY_TIME_, ENGINE_VERSION_)
    VALUES
        (v_dep, v_p_name, v_p_category, v_p_key, '', v_now, NULL);
    -- 2) BPMN å­—节(替换 key ä¸Žæ˜¾ç¤ºåï¼‰
    INSERT INTO ACT_GE_BYTEARRAY
        (ID_, NAME_, DEPLOYMENT_ID_, BYTES_, GENERATED_)
    SELECT
        v_byte_id,
        CONCAT(v_p_key, '.bpmn'),
        v_dep,
        CONVERT(
            REPLACE(
                REPLACE(CONVERT(BYTES_ USING utf8mb4), v_tpl_key, v_p_key),
                v_tpl_name, v_p_name)
        USING utf8mb4),
        0
      FROM ACT_GE_BYTEARRAY
     WHERE DEPLOYMENT_ID_ = v_tpl_dep
       AND NAME_ = CONCAT(v_tpl_key, '.bpmn');
    -- 3) æµç¨‹å®šä¹‰
    INSERT INTO ACT_RE_PROCDEF
        (ID_, REV_, CATEGORY_, NAME_, KEY_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_,
         DGRM_RESOURCE_NAME_, DESCRIPTION_, HAS_START_FORM_KEY_, HAS_GRAPHICAL_NOTATION_,
         SUSPENSION_STATE_, TENANT_ID_, ENGINE_VERSION_, DERIVED_VERSION_)
    VALUES
        (v_procdef_id, 1, v_p_category, v_p_name, v_p_key, 1, v_dep, CONCAT(v_p_key, '.bpmn'),
         NULL, NULL, 0, 1,
         1, '', NULL, 0);
    -- 4) æµç¨‹æ¨¡åž‹
    SET v_meta = CONCAT(
        '{"type":10,"formType":10,"sort":', v_sort,
        ',"visible":true,"allowCancelRunningProcess":true,"allowWithdrawTask":false',
        ',"autoApprovalType":0,"description":"","formCustomCreatePath":"","formCustomViewPath":"",',
        '"formId":4,"managerUserIds":[],"startUserIds":[],"startDeptIds":[],"icon":null,',
        '"processBeforeTriggerSetting":null,"processAfterTriggerSetting":null,',
        '"taskBeforeTriggerSetting":null,"taskAfterTriggerSetting":null,',
        '"printTemplateSetting":null,"summarySetting":null,"titleSetting":null,"processIdRule":null}');
    INSERT INTO ACT_RE_MODEL
        (ID_, NAME_, KEY_, CATEGORY_, CREATE_TIME_, LAST_UPDATE_TIME_, VERSION_, META_INFO_, DEPLOYMENT_ID_, TENANT_ID_)
    VALUES
        (v_model_id, v_p_name, v_p_key, v_p_category, v_now, v_now, 1, v_meta, v_dep, '');
    -- 5) æµç¨‹å®šä¹‰æ‰©å±•信息(通用审批表单 id=4)
    INSERT INTO bpm_process_definition_info
        (process_definition_id, model_id, model_type, category, form_type, form_id, visible, sort,
         allow_cancel_running_process, allow_withdraw_task,
         creator, create_time, updater, update_time, deleted)
    VALUES
        (v_procdef_id, v_model_id, 10, v_p_category, 10, 4, 1, v_sort,
         1, 1,
         '1', v_now, '1', v_now, 0);
    COMMIT;
    SELECT CONCAT('[OK] å·²éƒ¨ç½²æ–°å®¡æ ¸æµç¨‹ï¼š', v_p_key, ' (procdef=', v_procdef_id, ')') AS deploy_result;
END$$
DELIMITER ;
CALL bpm_deploy_srm_inquiry_approve();
-- æ ¡éªŒ
SELECT p.KEY_, p.NAME_, p.VERSION_, p.SUSPENSION_STATE_ AS active_flag,
       i.model_type, i.category, i.form_id
  FROM ACT_RE_PROCDEF p
  LEFT JOIN bpm_process_definition_info i ON i.process_definition_id = p.ID_
 WHERE p.KEY_ = 'srm_inquiry_approve';
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseRequestApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.erp.api.purchase;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestRespDTO;
/**
 * ERP é‡‡è´­ç”³è¯· API æŽ¥å£
 *
 * ä¾› SRM ç­‰æ¨¡å—在审批通过后发起询价、回写台账时调用。
 */
public interface ErpPurchaseRequestApi {
    String PREFIX = "/rpc-api/erp/purchase-request";
    /**
     * èŽ·å–é‡‡è´­ç”³è¯·ï¼ˆå«æ˜Žç»†ï¼‰
     *
     * @param id é‡‡è´­ç”³è¯·ç¼–号
     * @return é‡‡è´­ç”³è¯·
     */
    CommonResult<ErpPurchaseRequestRespDTO> getPurchaseRequest(Long id);
    /**
     * å›žå†™é‡‡è´­ç”³è¯·å…³è”的询价单
     *
     * @param id        é‡‡è´­ç”³è¯·ç¼–号
     * @param inquiryId è¯¢ä»·å•编号
     * @param inquiryNo è¯¢ä»·å•号
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> updatePurchaseRequestInquiry(Long id, Long inquiryId, String inquiryNo);
    /**
     * æ¸…除采购申请关联的询价单(询价单被取消/删除后,采购员可重新选择发起询价或直接生成台账)
     *
     * @param id é‡‡è´­ç”³è¯·ç¼–号
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> clearPurchaseRequestInquiry(Long id);
    /**
     * å›žå†™é‡‡è´­ç”³è¯·å…³è”的采购台账
     *
     * @param id      é‡‡è´­ç”³è¯·ç¼–号
     * @param orderId é‡‡è´­å°è´¦ç¼–号
     * @param orderNo é‡‡è´­å°è´¦å•号
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> updatePurchaseRequestOrder(Long id, Long orderId, String orderNo);
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseOrderCreateReqDTO.java
@@ -38,6 +38,9 @@
    @Schema(description = "来源单据编号", example = "1")
    private Long sourceId;
    @Schema(description = "来源采购申请编号", example = "1")
    private Long purchaseRequestId;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/dto/ErpPurchaseRequestRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
package cn.iocoder.yudao.module.erp.api.purchase.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
 * ERP é‡‡è´­ç”³è¯· Response DTO
 *
 * ä¾› SRM è¯¢ä»·æ¯”价等模块通过 RPC è¯»å–采购申请及其明细。
 */
@Data
public class ErpPurchaseRequestRespDTO {
    /**
     * ç¼–号
     */
    private Long id;
    /**
     * é‡‡è´­ç”³è¯·å•号
     */
    private String no;
    /**
     * çŠ¶æ€ï¼š0 æœªæäº¤ 10 å®¡æ‰¹ä¸­ 20 å®¡æ ¸é€šè¿‡ 30 å®¡æ ¸ä¸é€šè¿‡ 40 å·²å–消
     */
    private Integer status;
    /**
     * ç”³è¯·äººç¼–号
     */
    private Long requestUserId;
    /**
     * ç”³è¯·éƒ¨é—¨ç¼–号
     */
    private Long requestDeptId;
    /**
     * ç”³è¯·æ—¶é—´
     */
    private LocalDateTime requestTime;
    /**
     * ç”³è¯·ç†ç”±
     */
    private String requestReason;
    /**
     * ä¾›åº”商编号(可选)
     */
    private Long supplierId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * å·²ç”Ÿæˆçš„采购台账编号
     */
    private Long orderId;
    /**
     * å·²ç”Ÿæˆçš„采购台账单号
     */
    private String orderNo;
    /**
     * å·²å‘起的询价单编号
     */
    private Long inquiryId;
    /**
     * å·²å‘起的询价单号
     */
    private String inquiryNo;
    /**
     * ç”³è¯·æ˜Žç»†
     */
    private List<Item> items;
    @Data
    public static class Item {
        /**
         * æ˜Žç»†ç¼–号
         */
        private Long id;
        /**
         * äº§å“ç¼–号
         */
        private Long productId;
        /**
         * äº§å“å•位编号
         */
        private Long productUnitId;
        /**
         * æ•°é‡
         */
        private BigDecimal count;
        /**
         * éœ€æ±‚日期
         */
        private LocalDate demandTime;
        /**
         * æ˜¯å¦éœ€è¦æ¥æ–™æ£€éªŒ
         */
        private Boolean qcCheckFlag;
        /**
         * å¤‡æ³¨
         */
        private String remark;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/purchase/ErpPurchaseRequestApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.erp.api.purchase;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestRespDTO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * ERP é‡‡è´­ç”³è¯· API å®žçŽ°ç±»
 *
 * ä¾› SRM è¯¢ä»·æ¯”价模块读取采购申请、回写询价单与采购台账关联。
 */
@Service
@Validated
@Slf4j
public class ErpPurchaseRequestApiImpl implements ErpPurchaseRequestApi {
    @Resource
    private ErpPurchaseRequestMapper purchaseRequestMapper;
    @Resource
    private ErpPurchaseRequestItemMapper purchaseRequestItemMapper;
    @Override
    public CommonResult<ErpPurchaseRequestRespDTO> getPurchaseRequest(Long id) {
        ErpPurchaseRequestDO request = purchaseRequestMapper.selectById(id);
        if (request == null) {
            return success(null);
        }
        List<ErpPurchaseRequestItemDO> items = purchaseRequestItemMapper.selectListByRequestId(id);
        return success(BeanUtils.toBean(request, ErpPurchaseRequestRespDTO.class, resp -> resp
                .setItems(BeanUtils.toBean(items, ErpPurchaseRequestRespDTO.Item.class))));
    }
    @Override
    public CommonResult<Boolean> updatePurchaseRequestInquiry(Long id, Long inquiryId, String inquiryNo) {
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setInquiryId(inquiryId)
                .setInquiryNo(inquiryNo));
        return success(true);
    }
    @Override
    public CommonResult<Boolean> clearPurchaseRequestInquiry(Long id) {
        purchaseRequestMapper.update(null, new LambdaUpdateWrapper<ErpPurchaseRequestDO>()
                .eq(ErpPurchaseRequestDO::getId, id)
                .set(ErpPurchaseRequestDO::getInquiryId, null)
                .set(ErpPurchaseRequestDO::getInquiryNo, null));
        return success(true);
    }
    @Override
    public CommonResult<Boolean> updatePurchaseRequestOrder(Long id, Long orderId, String orderNo) {
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setOrderId(orderId)
                .setOrderNo(orderNo));
        return success(true);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
@@ -158,12 +158,11 @@
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<ErpPurchaseRequestImportExcelVO> list = Arrays.asList(
                ErpPurchaseRequestImportExcelVO.builder()
                        .no("CGSQ20260101000001").supplierName("示例供应商")
                        .no("CGSQ20260101000001")
                        .requestTime("2026-01-01 10:00:00").requestReason("示例:生产需要")
                        .productCode("ITEM001").productName("示例产品A")
                        .count(new BigDecimal("100")).productPrice(new BigDecimal("10.00"))
                        .taxPercent(new BigDecimal("13")).demandTime("2026-01-10")
                        .qcCheckFlag("否").remark("示例").build());
                        .count(new BigDecimal("100")).demandTime("2026-01-10")
                        .remark("示例").build());
        ExcelUtils.write(response, "采购申请导入模板.xls", "采购申请", ErpPurchaseRequestImportExcelVO.class, list);
    }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -106,6 +106,9 @@
    @Schema(description = "来源单据编号", example = "48")
    private Long sourceId;
    @Schema(description = "来源采购申请编号", example = "12")
    private Long purchaseRequestId;
    @Schema(description = "来源类型名称", example = "生产工单")
    private String sourceTypeName;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java
@@ -57,6 +57,9 @@
    @Schema(description = "来源单据编号", example = "48")
    private Long sourceId;
    @Schema(description = "来源采购申请编号", example = "1")
    private Long purchaseRequestId;
    @Schema(description = "备注", example = "你猜")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java
@@ -29,10 +29,6 @@
    @Schema(description = "采购申请单号,留空则自动生成;相同编号的多行合并为同一张申请")
    private String no;
    @ExcelProperty("供应商名称")
    @Schema(description = "供应商名称(可选,需与系统内供应商名称或编码一致)")
    private String supplierName;
    @ExcelProperty("申请时间")
    @Schema(description = "申请时间,格式如 2026-01-01 10:00:00;留空取当前时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private String requestTime;
@@ -53,21 +49,9 @@
    @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal count;
    @ExcelProperty("单价")
    @Schema(description = "产品参考单价;留空则取该物料的采购价")
    private BigDecimal productPrice;
    @ExcelProperty("税率(%)")
    @Schema(description = "税率,百分比,例如 13 è¡¨ç¤º 13%")
    private BigDecimal taxPercent;
    @ExcelProperty("需求日期")
    @Schema(description = "需求日期,格式 yyyy-MM-dd")
    private String demandTime;
    @ExcelProperty("是否来料检验")
    @Schema(description = "是否需要来料检验:填“是”为需要,其它为不需要")
    private String qcCheckFlag;
    @ExcelProperty("备注")
    @Schema(description = "备注;同一张申请取首行备注作为整单备注")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java
@@ -29,6 +29,12 @@
    @Schema(description = "供应商编号", example = "1")
    private Long supplierId;
    @Schema(description = "关联询价单号", example = "XJ2026110001")
    private String inquiryNo;
    @Schema(description = "生成的采购台账号", example = "CGDD001")
    private String orderNo;
    @Schema(description = "状态", example = "0")
    private Integer status;
@@ -42,4 +48,7 @@
    @Schema(description = "入库状态(0:未入库 1:部分入库 2:全部入库)", example = "0")
    private Integer inStatus;
    @Schema(description = "是否查询待处理申请(审批通过且未发起询价、未生成台账)", example = "true")
    private Boolean todo;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
@@ -64,17 +64,9 @@
    @ExcelProperty("供应商名称")
    private String supplierName;
    @Schema(description = "定金金额", example = "1000.00")
    @ExcelProperty("定金金额")
    private BigDecimal depositPrice;
    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("合计数量")
    private BigDecimal totalCount;
    @Schema(description = "合计价格", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("合计价格")
    private BigDecimal totalPrice;
    @Schema(description = "产品名称列表", example = "产品A,产品B")
    @ExcelProperty("产品名称列表")
@@ -87,6 +79,14 @@
    @Schema(description = "生成的采购订单号", example = "CGDD001")
    @ExcelProperty("采购订单号")
    private String orderNo;
    @Schema(description = "关联询价单编号", example = "1")
    @ExcelIgnore
    private Long inquiryId;
    @Schema(description = "关联询价单号", example = "XJ2026110001")
    @ExcelProperty("询价单号")
    private String inquiryNo;
    @Schema(description = "入库状态", example = "0")
    @ExcelProperty(value = "入库状态", converter = cn.iocoder.yudao.module.erp.convert.ErpPurchaseOrderInStatusConvert.class)
@@ -130,20 +130,8 @@
        @Schema(description = "产品单位名称", example = "个")
        private String productUnitName;
        @Schema(description = "产品参考单价", example = "100.00")
        private BigDecimal productPrice;
        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
        private BigDecimal count;
        @Schema(description = "总价", example = "1000.00")
        private BigDecimal totalPrice;
        @Schema(description = "税率,百分比", example = "13")
        private BigDecimal taxPercent;
        @Schema(description = "税额", example = "130.00")
        private BigDecimal taxPrice;
        @Schema(description = "需求日期")
        private LocalDate demandTime;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
@@ -25,14 +25,8 @@
    @Schema(description = "申请理由", example = "生产需要")
    private String requestReason;
    @Schema(description = "供应商编号", example = "1")
    @Schema(description = "供应商编号(可选,审批通过生成采购台账时可再选择)", example = "1")
    private Long supplierId;
    @Schema(description = "优惠率,百分比", example = "10")
    private BigDecimal discountPercent;
    @Schema(description = "定金金额", example = "1000.00")
    private BigDecimal depositPrice;
    @Schema(description = "备注", example = "备注")
    private String remark;
@@ -60,15 +54,9 @@
        @NotNull(message = "产品单位不能为空")
        private Long productUnitId;
        @Schema(description = "产品参考单价", example = "100.00")
        private BigDecimal productPrice;
        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
        @NotNull(message = "数量不能为空")
        private BigDecimal count;
        @Schema(description = "税率,百分比", example = "13")
        private BigDecimal taxPercent;
        @Schema(description = "需求日期")
        private LocalDate demandTime;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -135,6 +135,13 @@
    private Long sourceId;
    /**
     * æ¥æºé‡‡è´­ç”³è¯·ç¼–号
     *
     * ä¸€å¼ é‡‡è´­ç”³è¯·å¯èƒ½å› å¤šä¾›åº”商询价生成多张采购台账,通过该字段归集
     */
    private Long purchaseRequestId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java
@@ -75,36 +75,6 @@
    private BigDecimal totalCount;
    /**
     * åˆè®¡ä»·æ ¼
     */
    private BigDecimal totalPrice;
    /**
     * åˆè®¡äº§å“ä»·æ ¼
     */
    private BigDecimal totalProductPrice;
    /**
     * åˆè®¡ç¨Žé¢
     */
    private BigDecimal totalTaxPrice;
    /**
     * ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯”
     */
    private BigDecimal discountPercent;
    /**
     * ä¼˜æƒ é‡‘额
     */
    private BigDecimal discountPrice;
    /**
     * å®šé‡‘金额
     */
    private BigDecimal depositPrice;
    /**
     * å¤‡æ³¨
     */
    private String remark;
@@ -125,6 +95,16 @@
    private String orderNo;
    /**
     * å…³è”的询价单编号
     */
    private Long inquiryId;
    /**
     * å…³è”的询价单号
     */
    private String inquiryNo;
    /**
     * å…¥åº“状态
     *
     * 0: æœªå…¥åº“
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
@@ -44,29 +44,9 @@
    private Long productUnitId;
    /**
     * äº§å“å‚考单价
     */
    private BigDecimal productPrice;
    /**
     * æ•°é‡
     */
    private BigDecimal count;
    /**
     * æ€»ä»·
     */
    private BigDecimal totalPrice;
    /**
     * ç¨ŽçŽ‡ï¼Œç™¾åˆ†æ¯”
     */
    private BigDecimal taxPercent;
    /**
     * ç¨Žé¢
     */
    private BigDecimal taxPrice;
    /**
     * éœ€æ±‚日期
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java
@@ -31,15 +31,23 @@
    }
    default PageResult<ErpPurchaseRequestDO> selectPage(ErpPurchaseRequestPageReqVO pageReqVO) {
        return selectPage(pageReqVO, new LambdaQueryWrapperX<ErpPurchaseRequestDO>()
        LambdaQueryWrapperX<ErpPurchaseRequestDO> wrapper = new LambdaQueryWrapperX<ErpPurchaseRequestDO>()
                .likeIfPresent(ErpPurchaseRequestDO::getNo, pageReqVO.getNo())
                .eqIfPresent(ErpPurchaseRequestDO::getRequestUserId, pageReqVO.getRequestUserId())
                .eqIfPresent(ErpPurchaseRequestDO::getRequestDeptId, pageReqVO.getRequestDeptId())
                .eqIfPresent(ErpPurchaseRequestDO::getSupplierId, pageReqVO.getSupplierId())
                .likeIfPresent(ErpPurchaseRequestDO::getInquiryNo, pageReqVO.getInquiryNo())
                .likeIfPresent(ErpPurchaseRequestDO::getOrderNo, pageReqVO.getOrderNo())
                .eqIfPresent(ErpPurchaseRequestDO::getStatus, pageReqVO.getStatus())
                .eqIfPresent(ErpPurchaseRequestDO::getInStatus, pageReqVO.getInStatus())
                .betweenIfPresent(ErpPurchaseRequestDO::getRequestTime, pageReqVO.getRequestTime())
                .orderByDesc(ErpPurchaseRequestDO::getId));
                .betweenIfPresent(ErpPurchaseRequestDO::getRequestTime, pageReqVO.getRequestTime());
        // å¾…处理:审批通过后,尚未发起询价、也尚未生成采购台账
        if (Boolean.TRUE.equals(pageReqVO.getTodo())) {
            wrapper.isNull(ErpPurchaseRequestDO::getInquiryId)
                    .isNull(ErpPurchaseRequestDO::getOrderId);
        }
        wrapper.orderByDesc(ErpPurchaseRequestDO::getId);
        return selectPage(pageReqVO, wrapper);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseOrderSourceTypeEnum.java
@@ -17,7 +17,8 @@
    WORK_ORDER(1, "生产工单"),
    SRM_INQUIRY(2, "SRM询价"),
    SALE_ORDER(3, "销售订单");
    SALE_ORDER(3, "销售订单"),
    PURCHASE_REQUEST(4, "采购申请");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(ErpPurchaseOrderSourceTypeEnum::getType).toArray(Integer[]::new);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -34,6 +34,9 @@
    ErrorCode PURCHASE_ORDER_PURCHASE_TYPE_INVALID = new ErrorCode(1_030_101_016, "采购类型({})不合法,只能选择:1生产、2办公用品、3其他");
    ErrorCode PURCHASE_ORDER_PAYMENT_METHOD_INVALID = new ErrorCode(1_030_101_017, "付款方式({})不合法,只能选择:1电汇、2汇票、3现金、4手机支付");
    ErrorCode PURCHASE_ORDER_ITEM_PRODUCT_CODE_MISSING = new ErrorCode(1_030_101_018, "物料[{}]未维护 MDM ç‰©æ–™ç¼–码,请先在 MDM ç‰©æ–™æ¡£æ¡ˆè¡¥å……编码后再保存");
    ErrorCode PURCHASE_ORDER_SUBMIT_SUPPLIER_REQUIRED = new ErrorCode(1_030_101_019, "提交审批失败:采购台账({})未填写供应商,请先编辑台账选择供应商后再提交");
    ErrorCode PURCHASE_ORDER_SUBMIT_PRICE_REQUIRED = new ErrorCode(1_030_101_020, "提交审批失败:明细物料[{}]的单价为[{}],请先在采购台账明细中补录含税单价(大于 0)后再提交");
    ErrorCode PURCHASE_ORDER_SUBMIT_ITEM_REQUIRED = new ErrorCode(1_030_101_021, "提交审批失败:采购台账({})没有物料明细,请先补充明细后再提交");
    // ========== ERP é‡‡è´­å…¥åº“(1-030-102-000) ==========
    ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "采购入库单不存在");
@@ -58,7 +61,7 @@
    ErrorCode PURCHASE_REQUEST_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_104_008, "采购申请审批分类不存在,请先在BPM模块创建采购申请审批流程分类");
    ErrorCode PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_104_009, "采购申请审批分类下没有可用的流程模型,请先创建并发布流程");
    ErrorCode PURCHASE_REQUEST_RESET_FAIL_NOT_REJECT = new ErrorCode(1_030_104_010, "重置失败,只有驳回状态的采购申请单才能重置");
    ErrorCode PURCHASE_REQUEST_SUPPLIER_REQUIRED = new ErrorCode(1_030_104_011, "采购申请单未指定供应商,无法生成采购订单,请先编辑申请单补充供应商");
    ErrorCode PURCHASE_REQUEST_INQUIRY_EXISTS = new ErrorCode(1_030_104_012, "采购申请单已发起询价比价,请等待询价确认后再生成采购台账");
    // ========== ERP é‡‡è´­æ¥ç¥¨ï¼ˆ1-030-105-000) ==========
    ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "来票不存在");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -453,6 +453,43 @@
        }
    }
    /**
     * æäº¤å®¡æ‰¹å‰æ ¡éªŒé‡‡è´­å°è´¦æ•°æ®æ˜¯å¦å·²è¡¥é½
     *
     * <p>采购申请直接生成的台账只有物料与数量(零金额、无供应商),
     * é‡‡è´­å‘˜éœ€è¦åœ¨å°è´¦é‡Œè¡¥å½•供应商、单价、付款日期后才能提交审批。逐项校验,命中第一处即提示具体怎么改。</p>
     *
     * @param purchaseOrder é‡‡è´­å°è´¦
     */
    private void validatePurchaseOrderReadyForSubmit(ErpPurchaseOrderDO purchaseOrder) {
        // 2.1 ä¾›åº”商必选
        if (purchaseOrder.getSupplierId() == null) {
            throw exception(PURCHASE_ORDER_SUBMIT_SUPPLIER_REQUIRED, purchaseOrder.getNo());
        }
        // 2.2 ä»˜æ¬¾æ—¥æœŸå¿…选:采购申请自动生成的草稿订单没有付款日期
        if (purchaseOrder.getPaymentDate() == null) {
            throw exception(PURCHASE_ORDER_PAYMENT_DATE_REQUIRED);
        }
        // 2.3 æ˜Žç»†å•价必须补录(大于 0)
        List<ErpPurchaseOrderItemDO> items = purchaseOrderItemMapper.selectListByOrderId(purchaseOrder.getId());
        if (CollUtil.isEmpty(items)) {
            throw exception(PURCHASE_ORDER_SUBMIT_ITEM_REQUIRED, purchaseOrder.getNo());
        }
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(items, ErpPurchaseOrderItemDO::getProductId));
        for (ErpPurchaseOrderItemDO item : items) {
            BigDecimal productPrice = item.getProductPrice();
            if (productPrice != null && productPrice.compareTo(BigDecimal.ZERO) > 0) {
                continue;
            }
            MdmItemRespDTO itemDTO = itemMap.get(item.getProductId());
            String itemName = itemDTO != null && StrUtil.isNotBlank(itemDTO.getName())
                    ? itemDTO.getName() : "ID=" + item.getProductId();
            throw exception(PURCHASE_ORDER_SUBMIT_PRICE_REQUIRED, itemName,
                    productPrice == null ? "未填写" : productPrice.toPlainString());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitPurchaseOrder(Long id, String processDefinitionKey, Long userId) {
@@ -463,10 +500,9 @@
        if (!submitable) {
            throw exception(PURCHASE_ORDER_SUBMIT_FAIL_STATUS);
        }
        // 2. ä»˜æ¬¾æ—¥æœŸå¿…选:采购申请自动生成的草稿订单没有付款日期,提交前必须先编辑补齐
        if (purchaseOrder.getPaymentDate() == null) {
            throw exception(PURCHASE_ORDER_PAYMENT_DATE_REQUIRED);
        }
        // 2. æäº¤å‰æ ¡éªŒå°è´¦æ•°æ®å·²è¡¥é½ï¼šé‡‡è´­ç”³è¯·ç›´æŽ¥ç”Ÿæˆçš„草稿只有物料与数量,
        //    ä¾›åº”商、单价等需要采购员在台账中补录后才能提交审批
        validatePurchaseOrderReadyForSubmit(purchaseOrder);
        // 3. åˆ›å»º BPM æµç¨‹å®žä¾‹
        // å…¨å±€å…å®¡æ‰¹ï¼šä¸šåŠ¡æ ¡éªŒé€šè¿‡åŽç›´æŽ¥æ‰§è¡Œå®¡æ‰¹é€šè¿‡é€»è¾‘ï¼Œæ— éœ€æµç¨‹æ¨¡åž‹ã€‚
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -4,7 +4,6 @@
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
@@ -30,9 +29,8 @@
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderSourceTypeEnum;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -86,8 +84,6 @@
    @Resource
    private ErpSupplierService supplierService;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
@@ -128,7 +124,7 @@
                .setStatus(ErpPurchaseRequestStatusEnum.DRAFT.getStatus())
                .setRequestUserId(userId)
                .setRequestDeptId(deptId));
        calculateTotalPrice(purchaseRequest, requestItems);
        calculateTotalCount(purchaseRequest, requestItems);
        purchaseRequestMapper.insert(purchaseRequest);
        // 6. ç»‘定附件
@@ -156,7 +152,7 @@
        // 4. æ›´æ–°ç”³è¯·å•
        ErpPurchaseRequestDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseRequestDO.class);
        calculateTotalPrice(updateObj, requestItems);
        calculateTotalCount(updateObj, requestItems);
        purchaseRequestMapper.updateById(updateObj);
        // 5. æ›´æ–°ç”³è¯·æ˜Žç»†
@@ -182,39 +178,24 @@
            return respVO;
        }
        // 1. é¢„热“供应商名称/编码 -> ä¾›åº”商编号”映射(供应商为可选)
        Map<String, Long> supplierIdMap = new HashMap<>();
        CommonResult<List<SrmSupplierRespDTO>> supplierResult = srmSupplierApi.getSupplierSimpleList();
        if (supplierResult != null && CollUtil.isNotEmpty(supplierResult.getData())) {
            supplierResult.getData().forEach(supplier -> {
                if (StrUtil.isNotBlank(supplier.getName())) {
                    supplierIdMap.putIfAbsent(supplier.getName().trim(), supplier.getId());
                }
                if (StrUtil.isNotBlank(supplier.getCode())) {
                    supplierIdMap.putIfAbsent(supplier.getCode().trim(), supplier.getId());
                }
            });
        }
        // 2. æŒ‰â€œé‡‡è´­ç”³è¯·å•号”分组
        // 1. æŒ‰â€œé‡‡è´­ç”³è¯·å•号”分组
        Map<String, List<ErpPurchaseRequestImportExcelVO>> groups = new LinkedHashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            ErpPurchaseRequestImportExcelVO row = importList.get(i);
            if (row == null || (StrUtil.isBlank(row.getProductCode()) && StrUtil.isBlank(row.getSupplierName())
                    && StrUtil.isBlank(row.getNo()))) {
            if (row == null || (StrUtil.isBlank(row.getProductCode()) && StrUtil.isBlank(row.getNo()))) {
                continue; // è·³è¿‡ç©ºè¡Œ
            }
            String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2);
            groups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
        }
        // 3. é€å¼ ç”³è¯·å¯¼å…¥
        // 2. é€å¼ ç”³è¯·å¯¼å…¥
        for (Map.Entry<String, List<ErpPurchaseRequestImportExcelVO>> entry : groups.entrySet()) {
            List<ErpPurchaseRequestImportExcelVO> rows = entry.getValue();
            String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim()
                    : "第 " + entry.getKey().substring("__row_".length()) + " è¡Œ";
            try {
                ErpPurchaseRequestDO purchaseRequest = createPurchaseRequestByImport(rows, supplierIdMap);
                ErpPurchaseRequestDO purchaseRequest = createPurchaseRequestByImport(rows);
                respVO.getCreateList().add(purchaseRequest.getNo());
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
@@ -228,21 +209,10 @@
    /**
     * ä¾æ®å¯¼å…¥æ•°æ®åˆ›å»ºä¸€å¼ é‡‡è´­ç”³è¯·ï¼ˆå«æ˜Žç»†ï¼‰ã€‚申请人/申请部门由当前登录用户自动填充;状态为“未提交(0)”。
     */
    private ErpPurchaseRequestDO createPurchaseRequestByImport(List<ErpPurchaseRequestImportExcelVO> rows,
                                                              Map<String, Long> supplierIdMap) {
    private ErpPurchaseRequestDO createPurchaseRequestByImport(List<ErpPurchaseRequestImportExcelVO> rows) {
        ErpPurchaseRequestImportExcelVO head = rows.get(0);
        // 1. ä¾›åº”商(可选):填了则按名称/编码匹配并校验
        Long supplierId = null;
        if (StrUtil.isNotBlank(head.getSupplierName())) {
            supplierId = supplierIdMap.get(head.getSupplierName().trim());
            if (supplierId == null) {
                throw new IllegalArgumentException("供应商不存在:" + head.getSupplierName());
            }
            supplierService.validateSupplier(supplierId);
        }
        // 2. æ˜Žç»†
        // 1. æ˜Žç»†
        List<ErpPurchaseRequestSaveReqVO.Item> items = new ArrayList<>();
        for (ErpPurchaseRequestImportExcelVO row : rows) {
            if (StrUtil.isBlank(row.getProductCode())) {
@@ -262,13 +232,8 @@
            ErpPurchaseRequestSaveReqVO.Item itemVO = new ErpPurchaseRequestSaveReqVO.Item();
            itemVO.setProductId(item.getId());
            itemVO.setProductUnitId(item.getUnitMeasureId());
            // å•价:未填写时取物料的采购价
            itemVO.setProductPrice(row.getProductPrice() != null ? row.getProductPrice()
                    : (item.getPurchasePrice() != null ? item.getPurchasePrice() : BigDecimal.ZERO));
            itemVO.setCount(row.getCount());
            itemVO.setTaxPercent(row.getTaxPercent());
            itemVO.setDemandTime(parseImportDate(row.getDemandTime()));
            itemVO.setQcCheckFlag("是".equals(StrUtil.trim(row.getQcCheckFlag())));
            itemVO.setRemark(row.getRemark());
            items.add(itemVO);
        }
@@ -293,17 +258,15 @@
        // 5. ç»„装主表(状态为“未提交”)
        ErpPurchaseRequestDO purchaseRequest = new ErpPurchaseRequestDO();
        purchaseRequest.setNo(no);
        purchaseRequest.setSupplierId(supplierId);
        purchaseRequest.setRequestTime(parseImportDateTime(head.getRequestTime()));
        purchaseRequest.setRequestReason(head.getRequestReason());
        purchaseRequest.setRemark(head.getRemark());
        purchaseRequest.setStatus(ErpPurchaseRequestStatusEnum.DRAFT.getStatus());
        purchaseRequest.setRequestUserId(userId);
        purchaseRequest.setRequestDeptId(deptId);
        purchaseRequest.setDepositPrice(BigDecimal.ZERO);
        List<ErpPurchaseRequestItemDO> itemDOs = validatePurchaseRequestItems(items);
        calculateTotalPrice(purchaseRequest, itemDOs);
        calculateTotalCount(purchaseRequest, itemDOs);
        purchaseRequestMapper.insert(purchaseRequest);
        itemDOs.forEach(item -> item.setRequestId(purchaseRequest.getId()));
        purchaseRequestItemMapper.insertBatch(itemDOs);
@@ -359,30 +322,19 @@
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    private void calculateTotalPrice(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> requestItems) {
    private void calculateTotalCount(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> requestItems) {
        purchaseRequest.setTotalCount(getSumValue(requestItems, ErpPurchaseRequestItemDO::getCount, BigDecimal::add));
        purchaseRequest.setTotalProductPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalTaxPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
        // å•价为含税单价:价税合计 = å•ä»· * æ•°é‡ï¼Œç¨Žé¢å·²åŒ…含在单价中,不再额外加税
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalProductPrice());
        // è®¡ç®—优惠价格
        if (purchaseRequest.getDiscountPercent() == null) {
            purchaseRequest.setDiscountPercent(BigDecimal.ZERO);
        }
        purchaseRequest.setDiscountPrice(MoneyUtils.priceMultiplyPercent(purchaseRequest.getTotalPrice(), purchaseRequest.getDiscountPercent()));
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalPrice().subtract(purchaseRequest.getDiscountPrice()));
    }
    private List<ErpPurchaseRequestItemDO> validatePurchaseRequestItems(List<ErpPurchaseRequestSaveReqVO.Item> list) {
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseRequestItemDO.class, item -> {
            if (item.getProductPrice() != null && item.getCount() != null) {
                item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
                if (item.getTaxPercent() != null) {
                    // å•价为含税单价:税额从价税合计中反算,而不是在合计上再加税
                    item.setTaxPrice(MoneyUtils.taxPriceIncluded(item.getTotalPrice(), item.getTaxPercent()));
                }
        return convertList(list, o -> {
            ErpPurchaseRequestItemDO item = BeanUtils.toBean(o, ErpPurchaseRequestItemDO.class);
            // é‡‡è´­ç”³è¯·ä¸å†å¡«å†™â€œæ˜¯å¦è´¨æ£€â€ï¼Œé»˜è®¤éœ€è¦æ¥æ–™æ£€éªŒï¼Œç”Ÿæˆå°è´¦åŽå¯åœ¨å°è´¦ä¸­è°ƒæ•´
            if (item.getQcCheckFlag() == null) {
                item.setQcCheckFlag(true);
            }
        }));
            return item;
        });
    }
    private void updatePurchaseRequestItemList(Long id, List<ErpPurchaseRequestItemDO> newList) {
@@ -538,23 +490,11 @@
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        // å®¡æ‰¹é€šè¿‡åŽä¸è‡ªåŠ¨ç”Ÿæˆé‡‡è´­å°è´¦ï¼šç”±é‡‡è´­å‘˜åœ¨åˆ—è¡¨ä¸­å†³å®šã€Œå‘èµ·è¯¢ä»·ã€æˆ–ã€Œç›´æŽ¥ç”Ÿæˆé‡‡è´­å°è´¦ã€
        Integer newStatus = convertBpmResultToStatus(bpmResult);
        if (newStatus != null) {
            purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                    .setId(id).setStatus(newStatus));
        }
        // 3. å®¡æ‰¹é€šè¿‡ä¸”有供应商,自动生成采购订单
        if (ErpPurchaseRequestStatusEnum.APPROVE.getStatus().equals(newStatus)
                && purchaseRequest.getSupplierId() != null
                && purchaseRequest.getOrderId() == null) {
            try {
                Long orderId = doGeneratePurchaseOrder(purchaseRequest);
                log.info("[updatePurchaseRequestAuditStatus] é‡‡è´­ç”³è¯·({}) è‡ªåŠ¨ç”Ÿæˆé‡‡è´­è®¢å•({})", id, orderId);
            } catch (Exception e) {
                log.error("[updatePurchaseRequestAuditStatus] é‡‡è´­ç”³è¯·({}) è‡ªåŠ¨ç”Ÿæˆé‡‡è´­è®¢å•å¤±è´¥", id, e);
                // ä¸æŠ›å‡ºå¼‚常,避免影响状态更新
            }
        }
    }
@@ -570,15 +510,18 @@
        if (purchaseRequest.getOrderId() != null) {
            throw exception(PURCHASE_REQUEST_ORDER_EXISTS);
        }
        // 3. ä¾›åº”商以采购申请单为准:申请提交时已经选过供应商,生成订单不再要求重新选择
        //    å…¼å®¹æ—§è°ƒç”¨æ–¹ä»æ˜¾å¼ä¼  supplierId çš„æƒ…况,传了则以传入值为准
        Long targetSupplierId = supplierId != null ? supplierId : purchaseRequest.getSupplierId();
        if (targetSupplierId == null) {
            throw exception(PURCHASE_REQUEST_SUPPLIER_REQUIRED);
        // 3. å·²å‘起询价的申请,必须等询价确认(确定价格)后才能生成采购台账
        if (purchaseRequest.getInquiryId() != null) {
            throw exception(PURCHASE_REQUEST_INQUIRY_EXISTS);
        }
        supplierService.validateSupplier(targetSupplierId);
        // 4. ä¾›åº”商不在这里确定:采购申请本身不填供应商,直接生成台账时也允许为空,
        //    ç”±é‡‡è´­å‘˜åœ¨é‡‡è´­å°è´¦è‰ç¨¿é‡Œè¡¥å……供应商与价格后再提交审批
        Long targetSupplierId = supplierId != null ? supplierId : purchaseRequest.getSupplierId();
        if (targetSupplierId != null) {
            supplierService.validateSupplier(targetSupplierId);
        }
        // 4. è®¾ç½®ä¾›åº”商并生成订单
        // 5. ç”Ÿæˆè®¢å•(supplierId å¯ä¸ºç©ºï¼Œç•™å¾…台账补充)
        purchaseRequest.setSupplierId(targetSupplierId);
        return doGeneratePurchaseOrder(purchaseRequest);
    }
@@ -611,12 +554,17 @@
        purchaseOrder.setSupplierId(purchaseRequest.getSupplierId());
        purchaseOrder.setOrderTime(purchaseRequest.getRequestTime());
        purchaseOrder.setTotalCount(purchaseRequest.getTotalCount());
        purchaseOrder.setTotalPrice(purchaseRequest.getTotalPrice());
        purchaseOrder.setTotalProductPrice(purchaseRequest.getTotalProductPrice());
        purchaseOrder.setTotalTaxPrice(purchaseRequest.getTotalTaxPrice());
        purchaseOrder.setDiscountPercent(purchaseRequest.getDiscountPercent());
        purchaseOrder.setDiscountPrice(purchaseRequest.getDiscountPrice());
        purchaseOrder.setDepositPrice(purchaseRequest.getDepositPrice() != null ? purchaseRequest.getDepositPrice() : BigDecimal.ZERO);
        // é‡‡è´­ç”³è¯·ä¸å«ä»·æ ¼ï¼šå°è´¦å…ˆä»¥ 0 é‡‘额生成草稿,由采购员在台账中补录单价/税率后再提交审批
        purchaseOrder.setTotalPrice(BigDecimal.ZERO);
        purchaseOrder.setTotalProductPrice(BigDecimal.ZERO);
        purchaseOrder.setTotalTaxPrice(BigDecimal.ZERO);
        purchaseOrder.setDiscountPercent(BigDecimal.ZERO);
        purchaseOrder.setDiscountPrice(BigDecimal.ZERO);
        purchaseOrder.setDepositPrice(BigDecimal.ZERO);
        purchaseOrder.setSourceType(ErpPurchaseOrderSourceTypeEnum.PURCHASE_REQUEST.getType());
        purchaseOrder.setSourceId(purchaseRequest.getId());
        purchaseOrder.setSourceNo(purchaseRequest.getNo());
        purchaseOrder.setPurchaseRequestId(purchaseRequest.getId());
        purchaseOrder.setRemark(purchaseRequest.getRemark());
        purchaseOrder.setInCount(BigDecimal.ZERO);
        purchaseOrder.setInStatus(0);
@@ -629,11 +577,12 @@
            orderItem.setRequestItemId(item.getId()); // å…³è”采购申请明细
            orderItem.setProductId(item.getProductId());
            orderItem.setProductUnitId(item.getProductUnitId());
            orderItem.setProductPrice(item.getProductPrice());
            // å•ä»·/税率待采购员在台账中补录,先按 0 ä¿å­˜
            orderItem.setProductPrice(BigDecimal.ZERO);
            orderItem.setCount(item.getCount());
            orderItem.setTotalPrice(item.getTotalPrice());
            orderItem.setTaxPercent(item.getTaxPercent());
            orderItem.setTaxPrice(item.getTaxPrice());
            orderItem.setTotalPrice(BigDecimal.ZERO);
            orderItem.setTaxPercent(BigDecimal.ZERO);
            orderItem.setTaxPrice(BigDecimal.ZERO);
            orderItem.setRemark(item.getRemark());
            orderItem.setInCount(BigDecimal.ZERO);
            // æ˜¯å¦è´¨æ£€ä¸Žé‡‡è´­ç”³è¯·ä¿æŒä¸€è‡´ï¼šæ˜Žç»†æœªæ˜Žç¡®æ ‡è®°æ—¶é»˜è®¤ä¸ºã€Œæ˜¯ã€ï¼ˆåŽ†å²ç”³è¯·å•å¯èƒ½ä¸º null)
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/inquiry/SrmInquiryController.java
@@ -35,6 +35,14 @@
        return success(inquiryService.createInquiry(createReqVO));
    }
    @PostMapping("/create-from-request")
    @Operation(summary = "由采购申请发起询价")
    @Parameter(name = "requestId", description = "采购申请编号", required = true)
    @PreAuthorize("@ss.hasPermission('srm:inquiry:create')")
    public CommonResult<Long> createInquiryFromPurchaseRequest(@RequestParam("requestId") Long requestId) {
        return success(inquiryService.createInquiryFromPurchaseRequest(requestId));
    }
    @PutMapping("/update")
    @Operation(summary = "更新询价单(仅草稿)")
    @PreAuthorize("@ss.hasPermission('srm:inquiry:update')")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/inquiry/vo/SrmInquiryRespVO.java
@@ -25,6 +25,12 @@
    private Integer status;
    private String processInstanceId;
    @Schema(description = "关联采购申请编号")
    private Long purchaseRequestId;
    @Schema(description = "关联采购申请单号")
    private String purchaseRequestNo;
    @Schema(description = "询价日期")
    private LocalDate inquiryDate;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/dataobject/SrmInquiryDO.java
@@ -50,5 +50,13 @@
    private Long purchaserUserId;
    private String remark;
    private String processInstanceId;
    /**
     * å…³è”采购申请编号
     */
    private Long purchaseRequestId;
    /**
     * å…³è”采购申请单号
     */
    private String purchaseRequestNo;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java
@@ -36,5 +36,10 @@
    ErrorCode INQUIRY_ALREADY_CONFIRMED = new ErrorCode(1_040_600_014, "询价单已被确认转订单,请刷新后查看");
    ErrorCode INQUIRY_ORDER_GENERATE_FAIL = new ErrorCode(1_040_600_015, "询价单生成采购订单失败");
    ErrorCode INQUIRY_NO_GENERATE_FAIL = new ErrorCode(1_040_600_016, "询价单号生成失败");
    ErrorCode INQUIRY_PURCHASE_REQUEST_NOT_EXISTS = new ErrorCode(1_040_600_017, "采购申请不存在");
    ErrorCode INQUIRY_PURCHASE_REQUEST_NOT_APPROVED = new ErrorCode(1_040_600_018, "采购申请审核通过后才能发起询价");
    ErrorCode INQUIRY_PURCHASE_REQUEST_HAS_ORDER = new ErrorCode(1_040_600_019, "采购申请已生成采购台账,无法再发起询价");
    ErrorCode INQUIRY_PURCHASE_REQUEST_HAS_INQUIRY = new ErrorCode(1_040_600_020, "采购申请已发起过询价,请勿重复操作");
    ErrorCode INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY = new ErrorCode(1_040_600_021, "采购申请没有明细,无法发起询价");
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/inquiry/SrmInquiryService.java
@@ -14,6 +14,14 @@
    Long createInquiry(@Valid SrmInquirySaveReqVO createReqVO);
    /**
     * ç”±é‡‡è´­ç”³è¯·å‘起询价:按申请明细生成询价单草稿,并回写采购申请的询价单关联
     *
     * @param purchaseRequestId é‡‡è´­ç”³è¯·ç¼–号
     * @return è¯¢ä»·å•编号
     */
    Long createInquiryFromPurchaseRequest(Long purchaseRequestId);
    void updateInquiry(@Valid SrmInquirySaveReqVO updateReqVO);
    void deleteInquiry(Long id);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/inquiry/SrmInquiryServiceImpl.java
@@ -11,8 +11,10 @@
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseRequestApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
@@ -49,6 +51,11 @@
    private static final DateTimeFormatter NO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMM");
    /**
     * é‡‡è´­ç”³è¯·ã€Œå®¡æ ¸é€šè¿‡ã€çŠ¶æ€å€¼ï¼Œä¸Ž ERP æ¨¡å— ErpPurchaseRequestStatusEnum.APPROVE ä¿æŒä¸€è‡´
     */
    private static final Integer PURCHASE_REQUEST_STATUS_APPROVED = 20;
    @Resource
    private SrmInquiryMapper inquiryMapper;
    @Resource
@@ -69,6 +76,8 @@
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private ErpPurchaseOrderApi erpPurchaseOrderApi;
    @Resource
    private ErpPurchaseRequestApi erpPurchaseRequestApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -90,6 +99,73 @@
                createReqVO.getSuppliers().stream()
                        .map(SrmInquirySaveReqVO.Supplier::getSupplierId)
                        .collect(Collectors.toSet()));
        return inquiry.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createInquiryFromPurchaseRequest(Long purchaseRequestId) {
        ErpPurchaseRequestRespDTO purchaseRequest = erpPurchaseRequestApi
                .getPurchaseRequest(purchaseRequestId).getCheckedData();
        if (purchaseRequest == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_NOT_EXISTS);
        }
        if (!PURCHASE_REQUEST_STATUS_APPROVED.equals(purchaseRequest.getStatus())) {
            throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_NOT_APPROVED.getCode(),
                    "采购申请【" + purchaseRequest.getNo() + "】当前状态不是「审核通过」,无法发起询价。"
                            + "请先在采购申请中完成审批,审批通过后再发起询价。");
        }
        if (purchaseRequest.getOrderId() != null) {
            throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_HAS_ORDER.getCode(),
                    "采购申请【" + purchaseRequest.getNo() + "】已生成采购台账【"
                            + StrUtil.blankToDefault(purchaseRequest.getOrderNo(), "ID=" + purchaseRequest.getOrderId())
                            + "】,无法再发起询价。请到采购台账中查看价格明细。");
        }
        SrmInquiryDO linkedInquiry = purchaseRequest.getInquiryId() != null
                ? inquiryMapper.selectById(purchaseRequest.getInquiryId()) : null;
        if (linkedInquiry != null
                && !InquiryStatusEnum.CANCELLED.getStatus().equals(linkedInquiry.getStatus())) {
            throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_HAS_INQUIRY.getCode(),
                    "采购申请【" + purchaseRequest.getNo() + "】已发起过询价【"
                            + StrUtil.blankToDefault(purchaseRequest.getInquiryNo(), "ID=" + purchaseRequest.getInquiryId())
                            + "】,请直接打开该询价单继续处理,不要重复发起。");
        }
        List<ErpPurchaseRequestRespDTO.Item> requestItems = purchaseRequest.getItems();
        if (CollUtil.isEmpty(requestItems)) {
            throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY.getCode(),
                    "采购申请【" + purchaseRequest.getNo() + "】没有明细行,无法发起询价。请回到采购申请补充明细后再试。");
        }
        SrmInquiryDO inquiry = new SrmInquiryDO();
        inquiry.setNo(generateInquiryNo());
        inquiry.setTitle("采购申请【" + purchaseRequest.getNo() + "】询价");
        inquiry.setStatus(InquiryStatusEnum.DRAFT.getStatus());
        inquiry.setInquiryDate(LocalDate.now());
        inquiry.setPurchaserUserId(getLoginUserId());
        inquiry.setRemark("由采购申请【" + purchaseRequest.getNo() + "】发起");
        inquiry.setPurchaseRequestId(purchaseRequest.getId());
        inquiry.setPurchaseRequestNo(purchaseRequest.getNo());
        inquiryMapper.insert(inquiry);
        List<SrmInquirySaveReqVO.Item> itemVOs = new ArrayList<>();
        Set<Long> productIds = new HashSet<>();
        for (ErpPurchaseRequestRespDTO.Item requestItem : requestItems) {
            if (!productIds.add(requestItem.getProductId())) {
                throw new ServiceException(ErrorCodeConstants.INQUIRY_PURCHASE_REQUEST_ITEM_EMPTY.getCode(),
                        "采购申请【" + purchaseRequest.getNo() + "】中产品【"
                                + resolveItemName(requestItem.getProductId()) + "】存在多行明细,无法直接发起询价。"
                                + "请先在采购申请中合并同一产品的数量后再试。");
            }
            SrmInquirySaveReqVO.Item itemVO = new SrmInquirySaveReqVO.Item();
            itemVO.setProductId(requestItem.getProductId());
            itemVO.setProductUnitId(requestItem.getProductUnitId());
            itemVO.setQuantity(requestItem.getCount());
            itemVO.setRemark(requestItem.getRemark());
            itemVOs.add(itemVO);
        }
        saveItems(inquiry.getId(), itemVOs);
        erpPurchaseRequestApi.updatePurchaseRequestInquiry(purchaseRequest.getId(), inquiry.getId(), inquiry.getNo());
        return inquiry.getId();
    }
@@ -139,6 +215,7 @@
        inquirySupplierMapper.deleteByInquiryId(id);
        inquiryQuoteMapper.deleteByInquiryId(id);
        inquiryMapper.deleteById(id);
        clearPurchaseRequestLink(inquiry);
    }
    @Override
@@ -235,6 +312,8 @@
        }
        inquiryMapper.updateById(new SrmInquiryDO()
                .setId(id).setStatus(InquiryStatusEnum.CANCELLED.getStatus()));
        // è¯¢ä»·å–消后解除采购申请关联,采购员可重新选择「发起询价」或「直接生成台账」
        clearPurchaseRequestLink(inquiry);
    }
    @Override
@@ -458,6 +537,7 @@
            createReqDTO.setSourceType(2);
            createReqDTO.setSourceNo(inquiry.getNo());
            createReqDTO.setSourceId(inquiry.getId());
            createReqDTO.setPurchaseRequestId(inquiry.getPurchaseRequestId());
            createReqDTO.setRemark("SRM询价自动生成,询价单号:" + inquiry.getNo());
            createReqDTO.setItems(orderItems);
@@ -490,6 +570,28 @@
            orderResult.setPurchaseOrderId(purchaseOrderId);
            orderResult.setPurchaseOrderNo(purchaseOrderNo);
            orders.add(orderResult);
        }
        // 3.1 è¯¢ä»·å•来源于采购申请时,回写采购申请关联的采购台账
        //     ä¸€ä¸ªè¯¢ä»·å•可能按中标供应商拆成多张台账,采购申请只记录第一张,完整清单可通过台账的采购申请字段反查
        if (inquiry.getPurchaseRequestId() != null && CollUtil.isNotEmpty(orders)) {
            SrmInquiryConfirmRespVO.Order firstOrder = orders.get(0);
            if (orders.size() > 1) {
                log.info("[confirmInquiry] è¯¢ä»·å•({}) ä¸ºé‡‡è´­ç”³è¯·({}) ç”Ÿæˆ {} å¼ å°è´¦ï¼Œé‡‡è´­ç”³è¯·ä»…回写首张台账({})",
                        inquiry.getNo(), inquiry.getPurchaseRequestNo(), orders.size(), firstOrder.getPurchaseOrderNo());
            }
            try {
                erpPurchaseRequestApi.updatePurchaseRequestOrder(inquiry.getPurchaseRequestId(),
                        firstOrder.getPurchaseOrderId(), firstOrder.getPurchaseOrderNo());
            } catch (Exception ex) {
                log.error("[confirmInquiry] è¯¢ä»·å•({}) å›žå†™é‡‡è´­ç”³è¯·({}) å…³è”台账失败",
                        inquiry.getNo(), inquiry.getPurchaseRequestNo(), ex);
                throw new ServiceException(ErrorCodeConstants.INQUIRY_ORDER_GENERATE_FAIL.getCode(),
                        "询价单【" + inquiry.getNo() + "】已生成采购台账【"
                                + StrUtil.blankToDefault(firstOrder.getPurchaseOrderNo(), "ID=" + firstOrder.getPurchaseOrderId())
                                + "】,但回写采购申请【" + inquiry.getPurchaseRequestNo() + "】失败:" + ex.getMessage()
                                + "。请到采购申请中核对关联台账,或联系管理员处理。");
            }
        }
        // 4. æ›´æ–°è¯¢ä»·å•为已转订单(状态条件更新,避免并发重复生成)
@@ -726,6 +828,40 @@
                "询价单号在多次尝试后仍重复,请稍后重试。");
    }
    /**
     * è¯¢ä»·å•被取消/删除时,解除对采购申请的关联,让采购员可重新选择「发起询价」或「直接生成台账」。
     * è§£é™¤å¤±è´¥åªè®°å½•日志,不阻断取消操作;发起询价时会再按询价单状态做兜底判断。
     */
    private void clearPurchaseRequestLink(SrmInquiryDO inquiry) {
        if (inquiry.getPurchaseRequestId() == null) {
            return;
        }
        try {
            erpPurchaseRequestApi.clearPurchaseRequestInquiry(inquiry.getPurchaseRequestId());
        } catch (Exception ex) {
            log.warn("[clearPurchaseRequestLink] è¯¢ä»·å•({}) è§£é™¤é‡‡è´­ç”³è¯·({}) å…³è”失败,请人工核对",
                    inquiry.getNo(), inquiry.getPurchaseRequestNo(), ex);
        }
    }
    /**
     * è§£æžäº§å“åç§°ï¼Œä»…用于异常提示;解析失败时回退为可读 ID,避免二次异常
     */
    private String resolveItemName(Long productId) {
        if (productId == null) {
            return "未选择";
        }
        try {
            MdmItemRespDTO item = mdmItemApi.getItem(productId).getCheckedData();
            if (item != null && StrUtil.isNotBlank(item.getName())) {
                return item.getName();
            }
        } catch (Exception ex) {
            log.warn("[resolveItemName] æŸ¥è¯¢äº§å“({}) åç§°å¤±è´¥", productId, ex);
        }
        return "ID=" + productId;
    }
    private String statusName(Integer status) {
        for (InquiryStatusEnum e : InquiryStatusEnum.values()) {
            if (e.getStatus().equals(status)) {