tuqin
5 天以前 67d3ab41f4989faf4db5f7f95f56b8595dd8eb3b
feat(crm/erp): 新增采购申请、合同管理 Excel 导入(含菜单权限 SQL)

- 采购申请导入:模板/导入接口、按采购申请单号合并明细、客户/供应商按名称匹配、申请人按登录用户自动带出
- 合同管理导入:模板/导入接口、按合同编号合并明细、客户按名称匹配、金额按明细与优惠率自动计算、自动建数据权限
- 菜单权限:新增 erp:purchase-request:import、crm:contract:import
已添加6个文件
已修改6个文件
716 ■■■■■ 文件已修改
sql/mysql/crm_contract_import_menu.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/erp_purchase_request_import_menu.sql 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportExcelVO.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportRespVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/crm_contract_import_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
-- åˆåŒç®¡ç† - å¯¼å…¥æŒ‰é’®æƒé™ï¼ˆçˆ¶èœå•:合同管理 id=2398)
-- åŠ¨æ€ id(MAX(id)+1),避免与既有菜单撞车;可重复执行(幂等)。
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'crm:contract:import');
SET @import_menu_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @import_menu_id, '合同导入', 'crm:contract:import', 3, 8, 2398, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'crm:contract:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
sql/mysql/erp_purchase_request_import_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
-- é‡‡è´­ç”³è¯· - å¯¼å…¥æŒ‰é’®æƒé™ï¼ˆçˆ¶èœå•:采购申请 id=602498)
-- åŠ¨æ€ id(MAX(id)+1),避免与既有菜单撞车;可重复执行(幂等)。
SET @perm_exists := (SELECT COUNT(*) FROM system_menu WHERE permission = 'erp:purchase-request:import');
SET @import_menu_id := IFNULL((SELECT MAX(id) FROM system_menu), 0) + 1;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component,
                         component_name, status, visible, keep_alive, always_show,
                         creator, create_time, updater, update_time, deleted)
SELECT @import_menu_id, '采购申请导入', 'erp:purchase-request:import', 3, 8, 602498, '', '#', '', NULL,
       0, 1, 0, 1, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @perm_exists = 0;
SET @menu_id := (SELECT id FROM system_menu WHERE permission = 'erp:purchase-request:import');
SET @grant_exists := (SELECT COUNT(*) FROM system_role_menu WHERE role_id = 1 AND menu_id = @menu_id);
INSERT INTO system_role_menu (role_id, menu_id, creator, create_time, updater, update_time, deleted)
SELECT 1, @menu_id, '1', NOW(), '1', NOW(), 0
FROM DUAL
WHERE @menu_id IS NOT NULL AND @grant_exists = 0;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -13,6 +13,8 @@
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportExcelVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractTransferReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contact.CrmContactDO;
@@ -41,9 +43,11 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -178,6 +182,29 @@
        ExcelUtils.write(response, "合同.xls", "数据", CrmContractRespVO.class, list);
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入合同模板")
    @PreAuthorize("@ss.hasPermission('crm:contract:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<CrmContractImportExcelVO> list = Collections.singletonList(
                CrmContractImportExcelVO.builder()
                        .no("HT20260101000001").name("示例合同").customerName("示例客户")
                        .orderDate("2026-01-01 10:00:00").startTime("2026-01-01").endTime("2026-12-31")
                        .discountPercent(new BigDecimal("0")).depositPrice(new BigDecimal("0"))
                        .productCode("ITEM001").productName("示例产品A")
                        .count(100).contractPrice(new BigDecimal("20.00")).remark("示例数据,导入前请替换")
                        .build());
        ExcelUtils.write(response, "合同导入模板.xls", "合同", CrmContractImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入合同")
    @PreAuthorize("@ss.hasPermission('crm:contract:import')")
    public CommonResult<CrmContractImportRespVO> importContractExcel(@RequestParam("file") MultipartFile file) throws Exception {
        List<CrmContractImportExcelVO> list = ExcelUtils.read(file, CrmContractImportExcelVO.class);
        return success(contractService.importContractList(list));
    }
    @PutMapping("/transfer")
    @Operation(summary = "合同转移")
    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - åˆåŒå¯¼å…¥ Excel VO
 *
 * è¯´æ˜Žï¼šä¸€è¡Œ = ä¸€æ¡äº§å“æ˜Žç»†ï¼›ç›¸åŒâ€œåˆåŒç¼–号”的多行会合并为一张合同(主表字段取该组首行)。
 * â€œåˆåŒç¼–号”留空时,每一行会各自生成一张新合同(单条明细)。
 * è´Ÿè´£äººã€åˆåŒçŠ¶æ€ã€é‡‘é¢ ç”±ç³»ç»Ÿå¤„理:负责人 = å½“前登录用户;状态 = æœªæäº¤ï¼›é‡‘额按明细与优惠率自动计算。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - åˆåŒå¯¼å…¥ Excel VO")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CrmContractImportExcelVO {
    @ExcelProperty("合同编号")
    @Schema(description = "合同编号,留空则自动生成;相同编号的多行合并为同一张合同")
    private String no;
    @ExcelProperty("合同名称")
    @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED)
    private String name;
    @ExcelProperty("客户名称")
    @Schema(description = "客户名称,需与 CRM å®¢æˆ·åç§°ä¸€è‡´", requiredMode = Schema.RequiredMode.REQUIRED)
    private String customerName;
    @ExcelProperty("下单时间")
    @Schema(description = "下单时间,格式如 2026-01-01 10:00:00;留空取当前时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private String orderDate;
    @ExcelProperty("合同开始时间")
    @Schema(description = "合同开始时间,格式 yyyy-MM-dd")
    private String startTime;
    @ExcelProperty("合同结束时间")
    @Schema(description = "合同结束时间,格式 yyyy-MM-dd")
    private String endTime;
    @ExcelProperty("优惠率(%)")
    @Schema(description = "整单优惠率,百分比,例如 10 è¡¨ç¤º 10%;留空按 0")
    private BigDecimal discountPercent;
    @ExcelProperty("定金金额(元)")
    @Schema(description = "定金金额;留空按 0")
    private BigDecimal depositPrice;
    @ExcelProperty("产品编码")
    @Schema(description = "产品(物料)编码", requiredMode = Schema.RequiredMode.REQUIRED)
    private String productCode;
    @ExcelProperty("产品名称")
    @Schema(description = "产品名称(仅供对照,匹配仍以产品编码为准)")
    private String productName;
    @ExcelProperty("数量")
    @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED)
    private Integer count;
    @ExcelProperty("合同单价(元)")
    @Schema(description = "产品合同单价;留空则取该物料的销售价")
    private BigDecimal contractPrice;
    @ExcelProperty("备注")
    @Schema(description = "备注;同一张合同取首行备注作为整单备注")
    private String remark;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - åˆåŒå¯¼å…¥ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - åˆåŒå¯¼å…¥ Response VO")
@Data
@Builder
public class CrmContractImportRespVO {
    @Schema(description = "创建成功的合同编号数组", example = "HT20260101000001")
    private List<String> createList;
    @Schema(description = "更新成功的合同编号数组", example = "HT20260101000002")
    private List<String> updateList;
    @Schema(description = "导入失败集合,key ä¸ºåˆåŒç¼–号或行号,value ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
@@ -3,6 +3,8 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportExcelVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractTransferReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
@@ -34,6 +36,14 @@
    Long createContract(@Valid CrmContractSaveReqVO createReqVO, Long userId);
    /**
     * å¯¼å…¥åˆåŒ
     *
     * @param importList å¯¼å…¥æ•°æ®ï¼ˆæ¯ä¸€è¡Œ = ä¸€æ¡æ˜Žç»†ï¼Œç›¸åŒåˆåŒç¼–号合并为一张合同)
     * @return å¯¼å…¥ç»“æžœ
     */
    CrmContractImportRespVO importContractList(List<CrmContractImportExcelVO> importList);
    /**
     * æ›´æ–°åˆåŒ
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -1,9 +1,11 @@
package cn.iocoder.yudao.module.crm.service.contract;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
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;
@@ -15,13 +17,17 @@
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportExcelVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractImportRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractTransferReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractConfigDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationProductDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractProductMapper;
import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
@@ -59,9 +65,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.time.format.DateTimeFormatter;
import org.flowable.engine.repository.ProcessDefinition;
@@ -69,6 +77,7 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmResultToAuditStatus;
/**
@@ -98,6 +107,8 @@
    @Resource
    private CrmNoRedisDAO noRedisDAO;
    @Resource
    private CrmCustomerMapper customerMapper;
    @Resource
    private CrmPermissionService crmPermissionService;
@@ -259,6 +270,182 @@
        }));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CrmContractImportRespVO importContractList(List<CrmContractImportExcelVO> importList) {
        CrmContractImportRespVO respVO = CrmContractImportRespVO.builder()
                .createList(new ArrayList<>())
                .updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        // 1. é¢„热“客户名称 -> å®¢æˆ·ç¼–号”映射
        Map<String, Long> customerIdMap = new HashMap<>();
        List<CrmCustomerDO> customers = customerMapper.selectList();
        if (CollUtil.isNotEmpty(customers)) {
            customers.forEach(customer -> {
                if (StrUtil.isNotBlank(customer.getName())) {
                    customerIdMap.putIfAbsent(customer.getName().trim(), customer.getId());
                }
            });
        }
        // 2. æŒ‰â€œåˆåŒç¼–号”分组
        Map<String, List<CrmContractImportExcelVO>> groups = new LinkedHashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            CrmContractImportExcelVO row = importList.get(i);
            if (row == null || (StrUtil.isBlank(row.getName()) && StrUtil.isBlank(row.getCustomerName())
                    && 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. é€å¼ åˆåŒå¯¼å…¥
        for (Map.Entry<String, List<CrmContractImportExcelVO>> entry : groups.entrySet()) {
            List<CrmContractImportExcelVO> rows = entry.getValue();
            String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim()
                    : "第 " + entry.getKey().substring("__row_".length()) + " è¡Œ";
            try {
                CrmContractDO contract = createContractByImport(rows, customerIdMap);
                respVO.getCreateList().add(contract.getNo());
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * ä¾æ®å¯¼å…¥æ•°æ®åˆ›å»ºä¸€å¼ åˆåŒï¼ˆå«æ˜Žç»†ï¼‰ã€‚负责人=当前登录用户;状态=未提交;金额按明细与优惠率自动计算。
     */
    private CrmContractDO createContractByImport(List<CrmContractImportExcelVO> rows, Map<String, Long> customerIdMap) {
        CrmContractImportExcelVO head = rows.get(0);
        // 1. ä¸»è¡¨å¿…å¡«
        if (StrUtil.isBlank(head.getName())) {
            throw new IllegalArgumentException("合同名称不能为空");
        }
        if (StrUtil.isBlank(head.getCustomerName())) {
            throw new IllegalArgumentException("客户名称不能为空");
        }
        Long customerId = customerIdMap.get(head.getCustomerName().trim());
        if (customerId == null) {
            throw new IllegalArgumentException("客户不存在:" + head.getCustomerName());
        }
        customerService.validateCustomer(customerId);
        // 2. æ˜Žç»†
        List<CrmContractSaveReqVO.Product> products = new ArrayList<>();
        for (CrmContractImportExcelVO row : rows) {
            if (StrUtil.isBlank(row.getProductCode())) {
                throw new IllegalArgumentException("产品编码不能为空");
            }
            CommonResult<MdmItemRespDTO> itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim());
            MdmItemRespDTO item = itemResult == null ? null : itemResult.getData();
            if (item == null) {
                throw new IllegalArgumentException("产品不存在:" + row.getProductCode());
            }
            if (row.getCount() == null || row.getCount() <= 0) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量必须为正整数");
            }
            BigDecimal contractPrice = row.getContractPrice() != null ? row.getContractPrice()
                    : (item.getSalesPrice() != null ? item.getSalesPrice() : BigDecimal.ZERO);
            CrmContractSaveReqVO.Product product = new CrmContractSaveReqVO.Product();
            product.setItemId(item.getId());
            product.setItemPrice(item.getSalesPrice() != null ? item.getSalesPrice() : contractPrice);
            product.setContractPrice(contractPrice);
            product.setCount(row.getCount());
            products.add(product);
        }
        // 3. åˆåŒç¼–号
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(CrmNoRedisDAO.CONTRACT_NO_PREFIX);
        if (contractMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("合同编号已存在:" + no);
        }
        // 4. ç»„装主表(负责人=登录用户;状态=未提交)
        CrmContractDO contract = new CrmContractDO();
        contract.setNo(no);
        contract.setName(head.getName().trim());
        contract.setCustomerId(customerId);
        contract.setOwnerUserId(getLoginUserId());
        contract.setOrderDate(parseImportDateTime(head.getOrderDate()));
        contract.setStartTime(parseImportDate(head.getStartTime()));
        contract.setEndTime(parseImportDate(head.getEndTime()));
        contract.setDiscountPercent(head.getDiscountPercent() != null ? head.getDiscountPercent() : BigDecimal.ZERO);
        contract.setDepositPrice(head.getDepositPrice() != null ? head.getDepositPrice() : BigDecimal.ZERO);
        contract.setRemark(head.getRemark());
        contract.setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
        // 5. æ˜Žç»†è½åº“ + è‡ªåŠ¨ç®—ä»·
        List<CrmContractProductDO> productDOs = validateContractProducts(products);
        calculateTotalPrice(contract, productDOs);
        contractMapper.insert(contract);
        productDOs.forEach(product -> product.setContractId(contract.getId()));
        contractProductMapper.insertBatch(productDOs);
        // 6. åˆ›å»ºæ•°æ®æƒé™ï¼ˆå¦åˆ™å½“前用户看不到该合同)
        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(contract.getOwnerUserId())
                .setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        return contract;
    }
    private LocalDateTime parseImportDateTime(String text) {
        if (StrUtil.isBlank(text)) {
            return LocalDateTime.now();
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        throw new IllegalArgumentException("下单时间格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
    private LocalDateTime parseImportDate(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        } catch (Exception ignored) {
            // å¿½ç•¥
        }
        throw new IllegalArgumentException("日期格式不正确:" + text + ",示例 2026-01-01");
    }
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    private void calculateTotalPrice(CrmContractDO contract, List<CrmContractProductDO> contractProducts) {
        contract.setTotalProductPrice(getSumValue(contractProducts, CrmContractProductDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        BigDecimal discountPrice = MoneyUtils.priceMultiplyPercent(contract.getTotalProductPrice(), contract.getDiscountPercent());
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
@@ -11,6 +11,8 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO;
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.srm.api.supplier.SrmSupplierApi;
@@ -34,8 +36,11 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -133,6 +138,31 @@
        ExcelUtils.write(response, "采购申请.xls", "数据", ErpPurchaseRequestRespVO.class, list);
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入采购申请模板")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<ErpPurchaseRequestImportExcelVO> list = Arrays.asList(
                ErpPurchaseRequestImportExcelVO.builder()
                        .no("CGSQ20260101000001").supplierName("示例供应商")
                        .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());
        ExcelUtils.write(response, "采购申请导入模板.xls", "采购申请", ErpPurchaseRequestImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入采购申请")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:import')")
    public CommonResult<ErpPurchaseRequestImportRespVO> importPurchaseRequestExcel(
            @RequestParam("file") MultipartFile file) throws Exception {
        List<ErpPurchaseRequestImportExcelVO> list = ExcelUtils.read(file, ErpPurchaseRequestImportExcelVO.class);
        return success(purchaseRequestService.importPurchaseRequestList(list));
    }
    @PutMapping("/submit")
    @Operation(summary = "提交采购申请审批")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:submit')")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * ç®¡ç†åŽå° - é‡‡è´­ç”³è¯·å¯¼å…¥ Excel VO
 *
 * è¯´æ˜Žï¼šä¸€è¡Œ = ä¸€æ¡äº§å“æ˜Žç»†ï¼›ç›¸åŒâ€œé‡‡è´­ç”³è¯·å•号”的多行会合并为一张申请(主表字段取该组首行)。
 * â€œé‡‡è´­ç”³è¯·å•号”留空时,每一行会各自生成一张新申请(单条明细)。
 * ç”³è¯·äºº / ç”³è¯·éƒ¨é—¨ ç”±ç³»ç»ŸæŒ‰â€œå½“前登录用户”自动填充,无需填写。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é‡‡è´­ç”³è¯·å¯¼å…¥ Excel VO")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ErpPurchaseRequestImportExcelVO {
    @ExcelProperty("采购申请单号")
    @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;
    @ExcelProperty("申请理由")
    @Schema(description = "申请理由")
    private String requestReason;
    @ExcelProperty("产品编码")
    @Schema(description = "产品(物料)编码", requiredMode = Schema.RequiredMode.REQUIRED)
    private String productCode;
    @ExcelProperty("产品名称")
    @Schema(description = "产品名称(仅供对照,匹配仍以产品编码为准)")
    private String productName;
    @ExcelProperty("数量")
    @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 = "备注;同一张申请取首行备注作为整单备注")
    private String remark;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
 * ç®¡ç†åŽå° - é‡‡è´­ç”³è¯·å¯¼å…¥ Response VO
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - é‡‡è´­ç”³è¯·å¯¼å…¥ Response VO")
@Data
@Builder
public class ErpPurchaseRequestImportRespVO {
    @Schema(description = "创建成功的采购申请单号数组", example = "CGSQ20260101000001")
    private List<String> createList;
    @Schema(description = "更新成功的采购申请单号数组", example = "CGSQ20260101000002")
    private List<String> updateList;
    @Schema(description = "导入失败集合,key ä¸ºé‡‡è´­ç”³è¯·å•号或行号,value ä¸ºå¤±è´¥åŽŸå› ")
    private Map<String, String> failureList;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java
@@ -3,6 +3,8 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import jakarta.validation.Valid;
@@ -25,6 +27,14 @@
    Long createPurchaseRequest(@Valid ErpPurchaseRequestSaveReqVO createReqVO);
    /**
     * å¯¼å…¥é‡‡è´­ç”³è¯·
     *
     * @param importList å¯¼å…¥æ•°æ®ï¼ˆæ¯ä¸€è¡Œ = ä¸€æ¡æ˜Žç»†ï¼Œç›¸åŒé‡‡è´­ç”³è¯·å•号合并为一张申请)
     * @return å¯¼å…¥ç»“æžœ
     */
    ErpPurchaseRequestImportRespVO importPurchaseRequestList(List<ErpPurchaseRequestImportExcelVO> importList);
    /**
     * æ›´æ–°é‡‡è´­ç”³è¯·
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.hutool.core.collection.CollUtil;
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;
@@ -13,6 +15,8 @@
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportExcelVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestImportRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
@@ -22,8 +26,12 @@
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
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.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;
@@ -36,11 +44,15 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
/**
 * ERP é‡‡è´­ç”³è¯· Service å®žçŽ°ç±»
@@ -70,6 +82,10 @@
    @Resource
    private ErpSupplierService supplierService;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
@@ -149,6 +165,188 @@
        storageAttachmentApi.updateAttachments("file", "erp_purchase_request", updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ErpPurchaseRequestImportRespVO importPurchaseRequestList(List<ErpPurchaseRequestImportExcelVO> importList) {
        ErpPurchaseRequestImportRespVO respVO = ErpPurchaseRequestImportRespVO.builder()
                .createList(new ArrayList<>())
                .updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>())
                .build();
        if (CollUtil.isEmpty(importList)) {
            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. æŒ‰â€œé‡‡è´­ç”³è¯·å•号”分组
        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()))) {
                continue; // è·³è¿‡ç©ºè¡Œ
            }
            String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2);
            groups.computeIfAbsent(key, k -> new ArrayList<>()).add(row);
        }
        // 3. é€å¼ ç”³è¯·å¯¼å…¥
        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);
                respVO.getCreateList().add(purchaseRequest.getNo());
            } catch (Exception ex) {
                respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
    /**
     * ä¾æ®å¯¼å…¥æ•°æ®åˆ›å»ºä¸€å¼ é‡‡è´­ç”³è¯·ï¼ˆå«æ˜Žç»†ï¼‰ã€‚申请人/申请部门由当前登录用户自动填充;状态为“未提交(0)”。
     */
    private ErpPurchaseRequestDO createPurchaseRequestByImport(List<ErpPurchaseRequestImportExcelVO> rows,
                                                              Map<String, Long> supplierIdMap) {
        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. æ˜Žç»†
        List<ErpPurchaseRequestSaveReqVO.Item> items = new ArrayList<>();
        for (ErpPurchaseRequestImportExcelVO row : rows) {
            if (StrUtil.isBlank(row.getProductCode())) {
                throw new IllegalArgumentException("产品编码不能为空");
            }
            CommonResult<MdmItemRespDTO> itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim());
            MdmItemRespDTO item = itemResult == null ? null : itemResult.getData();
            if (item == null) {
                throw new IllegalArgumentException("产品不存在:" + row.getProductCode());
            }
            if (row.getCount() == null) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量不能为空");
            }
            if (item.getUnitMeasureId() == null) {
                throw new IllegalArgumentException("产品[" + row.getProductCode() + "]未配置计量单位,请先在物料中维护");
            }
            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);
        }
        // 3. å•号:填了就用(校验唯一),没填自动生成 CGSQ...
        String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim()
                : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX);
        if (purchaseRequestMapper.selectByNo(no) != null) {
            throw new IllegalArgumentException("采购申请单号已存在:" + no);
        }
        // 4. ç”³è¯·äºº / ç”³è¯·éƒ¨é—¨ï¼šå–当前登录用户及其部门
        Long userId = getLoginUserId();
        AdminUserRespDTO user = userId == null ? null : adminUserApi.getUser(userId);
        Long deptId = user != null ? user.getDeptId() : null;
        // 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);
        purchaseRequestMapper.insert(purchaseRequest);
        itemDOs.forEach(item -> item.setRequestId(purchaseRequest.getId()));
        purchaseRequestItemMapper.insertBatch(itemDOs);
        return purchaseRequest;
    }
    private LocalDateTime parseImportDateTime(String text) {
        if (StrUtil.isBlank(text)) {
            return LocalDateTime.now();
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        throw new IllegalArgumentException("申请时间格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
    private LocalDate parseImportDate(String text) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // ç»§ç»­å°è¯•下一个格式
            }
        }
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")).toLocalDate();
        } catch (Exception ignored) {
            // å¿½ç•¥
        }
        throw new IllegalArgumentException("需求日期格式不正确:" + text + ",示例 2026-01-01");
    }
    private String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
    private void calculateTotalPrice(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> requestItems) {
        purchaseRequest.setTotalCount(getSumValue(requestItems, ErpPurchaseRequestItemDO::getCount, BigDecimal::add));
        purchaseRequest.setTotalProductPrice(getSumValue(requestItems, ErpPurchaseRequestItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));