From 67d3ab41f4989faf4db5f7f95f56b8595dd8eb3b Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期五, 11 九月 2026 09:39:44 +0800
Subject: [PATCH] feat(crm/erp): 新增采购申请、合同管理 Excel 导入(含菜单权限 SQL)
---
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java | 10
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportRespVO.java | 29 ++
sql/mysql/erp_purchase_request_import_menu.sql | 20 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportRespVO.java | 29 ++
sql/mysql/crm_contract_import_menu.sql | 20 +
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java | 10
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java | 198 ++++++++++++++++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java | 30 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java | 27 ++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java | 187 +++++++++++++++
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java | 76 ++++++
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportExcelVO.java | 80 ++++++
12 files changed, 716 insertions(+), 0 deletions(-)
diff --git a/sql/mysql/crm_contract_import_menu.sql b/sql/mysql/crm_contract_import_menu.sql
new file mode 100644
index 0000000..e79ffef
--- /dev/null
+++ b/sql/mysql/crm_contract_import_menu.sql
@@ -0,0 +1,20 @@
+-- 鍚堝悓绠$悊 - 瀵煎叆鎸夐挳鏉冮檺锛堢埗鑿滃崟锛氬悎鍚岀鐞� id=2398锛�
+-- 鍔ㄦ�� id锛圡AX(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;
diff --git a/sql/mysql/erp_purchase_request_import_menu.sql b/sql/mysql/erp_purchase_request_import_menu.sql
new file mode 100644
index 0000000..ed0f2d0
--- /dev/null
+++ b/sql/mysql/erp_purchase_request_import_menu.sql
@@ -0,0 +1,20 @@
+-- 閲囪喘鐢宠 - 瀵煎叆鎸夐挳鏉冮檺锛堢埗鑿滃崟锛氶噰璐敵璇� id=602498锛�
+-- 鍔ㄦ�� id锛圡AX(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;
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
index 45b5f89..ff5e1c6 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
+++ b/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')")
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportExcelVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportExcelVO.java
new file mode 100644
index 0000000..4a3d137
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractImportRespVO.java
new file mode 100644
index 0000000..7a3f89c
--- /dev/null
+++ b/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 = "瀵煎叆澶辫触闆嗗悎锛宬ey 涓哄悎鍚岀紪鍙锋垨琛屽彿锛寁alue 涓哄け璐ュ師鍥�")
+ private Map<String, String> failureList;
+
+}
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
index 80693c2..b1390b6 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
+++ b/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 鏇存柊淇℃伅
diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
index 88705a1..448dae2 100644
--- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
+++ b/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;
+ }
+
+ /**
+ * 渚濇嵁瀵煎叆鏁版嵁鍒涘缓涓�寮犲悎鍚岋紙鍚槑缁嗭級銆傝礋璐d汉=褰撳墠鐧诲綍鐢ㄦ埛锛涚姸鎬�=鏈彁浜わ紱閲戦鎸夋槑缁嗕笌浼樻儬鐜囪嚜鍔ㄨ绠椼��
+ */
+ 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() + "]鐨勬暟閲忓繀椤讳负姝f暣鏁�");
+ }
+ 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. 缁勮涓昏〃锛堣礋璐d汉=鐧诲綍鐢ㄦ埛锛涚姸鎬�=鏈彁浜わ級
+ 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());
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
index 2d41a03..0175677 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
+++ b/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')")
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportExcelVO.java
new file mode 100644
index 0000000..f1903fd
--- /dev/null
+++ b/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;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestImportRespVO.java
new file mode 100644
index 0000000..3dc6fa6
--- /dev/null
+++ b/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 = "瀵煎叆澶辫触闆嗗悎锛宬ey 涓洪噰璐敵璇峰崟鍙锋垨琛屽彿锛寁alue 涓哄け璐ュ師鍥�")
+ private Map<String, String> failureList;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java
index f06dc72..6f4cc7b 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java
+++ b/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 鏇存柊淇℃伅
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
index 681e96a..54d6447 100644
--- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
+++ b/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("闇�姹傛棩鏈熸牸寮忎笉姝g‘锛�" + 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));
--
Gitblit v1.9.3