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));