From b3fe06c151dc8455649bf0a46e481ceb83e3ea2a Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期六, 07 十一月 2026 13:37:54 +0800
Subject: [PATCH] feat: 完成好水川 MOM 需求1/2/4/5/8(装箱、BOM成本、销售合同、劳动合同、留样)

---
 yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java |  139 ++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 126 insertions(+), 13 deletions(-)

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 448dae2..5561170 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
@@ -10,11 +10,13 @@
 import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
 import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
 import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.crm.api.contract.dto.CrmContractCreateFromSaleOrderReqDTO;
 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;
@@ -56,6 +58,7 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
@@ -272,6 +275,8 @@
 
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_IMPORT_SUB_TYPE, bizNo = "0",
+            success = CRM_CONTRACT_IMPORT_SUCCESS)
     public CrmContractImportRespVO importContractList(List<CrmContractImportExcelVO> importList) {
         CrmContractImportRespVO respVO = CrmContractImportRespVO.builder()
                 .createList(new ArrayList<>())
@@ -279,6 +284,8 @@
                 .failureList(new LinkedHashMap<>())
                 .build();
         if (CollUtil.isEmpty(importList)) {
+            // 绌烘枃浠朵篃瑕佺粰鏃ュ織鏂囨鍏滃簳锛岄伩鍏� {{#importSummary}} 鍙栦笉鍒板��
+            OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList());
             return respVO;
         }
 
@@ -317,6 +324,8 @@
                 respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
             }
         }
+        // 璁板綍瀵煎叆缁撴灉姹囨�伙紝渚� @LogRecord 鐨� success 鏂囨寮曠敤
+        OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList());
         return respVO;
     }
 
@@ -327,46 +336,58 @@
         CrmContractImportExcelVO head = rows.get(0);
         // 1. 涓昏〃蹇呭~
         if (StrUtil.isBlank(head.getName())) {
-            throw new IllegalArgumentException("鍚堝悓鍚嶇О涓嶈兘涓虹┖");
+            throw new IllegalArgumentException("鍚堝悓鍚嶇О涓嶈兘涓虹┖锛氳濉啓妯℃澘涓琛岀殑銆屽悎鍚屽悕绉般�嶅垪锛�"
+                    + "鑻ュ悓涓�寮犲悎鍚岀殑澶氳浣跨敤浜嗗悎骞跺崟鍏冩牸锛岄渶濉啓鍦ㄥ悎骞跺尯鍩熺殑棣栬");
         }
         if (StrUtil.isBlank(head.getCustomerName())) {
-            throw new IllegalArgumentException("瀹㈡埛鍚嶇О涓嶈兘涓虹┖");
+            throw new IllegalArgumentException("瀹㈡埛鍚嶇О涓嶈兘涓虹┖锛氳濉啓妯℃澘涓琛岀殑銆屽鎴峰悕绉般�嶅垪锛�"
+                    + "鑻ュ悓涓�寮犲悎鍚岀殑澶氳浣跨敤浜嗗悎骞跺崟鍏冩牸锛岄渶濉啓鍦ㄥ悎骞跺尯鍩熺殑棣栬");
         }
         Long customerId = customerIdMap.get(head.getCustomerName().trim());
         if (customerId == null) {
-            throw new IllegalArgumentException("瀹㈡埛涓嶅瓨鍦細" + head.getCustomerName());
+            throw new IllegalArgumentException("瀹㈡埛涓嶅瓨鍦細" + head.getCustomerName()
+                    + "銆傝鍏堝湪銆孋RM 瀹㈡埛绠$悊銆嶄腑鍒涘缓璇ュ鎴凤紙鍚嶇О闇�涓庢ā鏉垮畬鍏ㄤ竴鑷达級锛屾垨淇敼妯℃澘涓殑瀹㈡埛鍚嶇О鍚庨噸鏂板鍏�");
         }
         customerService.validateCustomer(customerId);
 
         // 2. 鏄庣粏
-        List<CrmContractSaveReqVO.Product> products = new ArrayList<>();
+        List<CrmContractProductDO> productDOs = new ArrayList<>();
         for (CrmContractImportExcelVO row : rows) {
             if (StrUtil.isBlank(row.getProductCode())) {
-                throw new IllegalArgumentException("浜у搧缂栫爜涓嶈兘涓虹┖");
+                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());
+                throw new IllegalArgumentException("浜у搧涓嶅瓨鍦細" + row.getProductCode()
+                        + "銆傝鍏堝湪銆岀墿鏂欑鐞嗐�嶄腑缁存姢璇ョ墿鏂欙紙缂栫爜闇�涓庢ā鏉垮畬鍏ㄤ竴鑷达級锛屾垨淇敼妯℃澘涓殑浜у搧缂栫爜鍚庨噸鏂板鍏�");
             }
-            if (row.getCount() == null || row.getCount() <= 0) {
-                throw new IllegalArgumentException("浜у搧[" + row.getProductCode() + "]鐨勬暟閲忓繀椤讳负姝f暣鏁�");
+            if (row.getCount() == null) {
+                throw new IllegalArgumentException("浜у搧[" + row.getProductCode()
+                        + "]鐨勬暟閲忎笉鑳戒负绌猴細璇峰~鍐欐ā鏉夸腑璇ヨ鐨勩�屾暟閲忋�嶅垪");
+            }
+            if (row.getCount().compareTo(BigDecimal.ZERO) <= 0) {
+                throw new IllegalArgumentException("浜у搧[" + row.getProductCode() + "]鐨勬暟閲忓繀椤诲ぇ浜� 0锛屽綋鍓嶄负 "
+                        + row.getCount().stripTrailingZeros().toPlainString() + "銆傝淇敼妯℃澘鍚庨噸鏂板鍏�");
             }
             BigDecimal contractPrice = row.getContractPrice() != null ? row.getContractPrice()
                     : (item.getSalesPrice() != null ? item.getSalesPrice() : BigDecimal.ZERO);
-            CrmContractSaveReqVO.Product product = new CrmContractSaveReqVO.Product();
+            CrmContractProductDO product = new CrmContractProductDO();
             product.setItemId(item.getId());
+            product.setItemUnitId(item.getUnitMeasureId());
             product.setItemPrice(item.getSalesPrice() != null ? item.getSalesPrice() : contractPrice);
             product.setContractPrice(contractPrice);
             product.setCount(row.getCount());
-            products.add(product);
+            product.setTotalPrice(MoneyUtils.priceMultiply(contractPrice, row.getCount()));
+            productDOs.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);
+            throw new IllegalArgumentException("鍚堝悓缂栧彿宸插瓨鍦細" + no
+                    + "銆傝淇敼妯℃澘涓殑鍚堝悓缂栧彿锛屾垨鍏堝湪绯荤粺涓垹闄よ缂栧彿鐨勫悎鍚屽悗閲嶆柊瀵煎叆");
         }
 
         // 4. 缁勮涓昏〃锛堣礋璐d汉=鐧诲綍鐢ㄦ埛锛涚姸鎬�=鏈彁浜わ級
@@ -384,7 +405,6 @@
         contract.setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
 
         // 5. 鏄庣粏钀藉簱 + 鑷姩绠椾环
-        List<CrmContractProductDO> productDOs = validateContractProducts(products);
         calculateTotalPrice(contract, productDOs);
         contractMapper.insert(contract);
         productDOs.forEach(product -> product.setContractId(contract.getId()));
@@ -472,7 +492,9 @@
         if (quotation != null) {
             crmSaleQuotationService.clearContractReference(quotation.getId());
         }
-        // 2.2 鍒犻櫎鏁版嵁鏉冮檺
+        // 2.3 鍒犻櫎鍚堝悓浜у搧鏄庣粏
+        contractProductMapper.delete(CrmContractProductDO::getContractId, id);
+        // 2.4 鍒犻櫎鏁版嵁鏉冮檺
         crmPermissionService.deletePermission(CrmBizTypeEnum.CRM_CONTRACT.getType(), id);
 
         // 3. 璁板綍鎿嶄綔鏃ュ織涓婁笅鏂�
@@ -533,6 +555,15 @@
         }
 
         // 2. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+        // 鍏ㄥ眬鍏嶅鎵癸細涓氬姟鏍¢獙閫氳繃鍚庣洿鎺ユ墽琛屽鎵归�氳繃閫昏緫锛屾棤闇�娴佺▼妯″瀷銆�
+        if (bpmProcessInstanceApi.isGlobalAutoApprove()) {
+            contractMapper.updateById(new CrmContractDO().setId(id).setProcessInstanceId("")
+                    .setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()));
+            updateContractAuditStatus(id, cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+            LogRecordContext.putVariable("contractName", contract.getName());
+            return;
+        }
+
         validateContractApproveCategoryAndProcessDefinition();
 
         // 3. 鏍¢獙浼犲叆鐨勬祦绋嬪畾涔� Key 鏄惁灞炰簬璇ュ垎绫讳笖澶勪簬婵�娲荤姸鎬�
@@ -644,6 +675,11 @@
     }
 
     @Override
+    public CrmContractDO getContractByNo(String no) {
+        return contractMapper.selectByNo(no);
+    }
+
+    @Override
     public CrmContractDO validateContract(Long id) {
         return validateContractExists(id);
     }
@@ -700,6 +736,16 @@
     @Override
     public Long getContractCountByBusinessId(Long businessId) {
         return contractMapper.selectCountByBusinessId(businessId);
+    }
+
+    @Override
+    public Map<Long, Long> getContractCountMapByCustomerIds(Collection<Long> customerIds) {
+        return contractMapper.selectContractCountMapByCustomerIds(customerIds);
+    }
+
+    @Override
+    public Map<Long, BigDecimal> getContractPriceMapByCustomerIds(Collection<Long> customerIds) {
+        return contractMapper.selectContractPriceMapByCustomerIds(customerIds);
     }
 
     @Override
@@ -790,6 +836,73 @@
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
+    public CrmContractDO createContractDraftFromSaleOrder(CrmContractCreateFromSaleOrderReqDTO reqDTO) {
+        // 1. 骞傜瓑锛氬悓涓�閿�鍞鍗曞彧鐢熸垚涓�寮犲悎鍚�
+        CrmContractDO existed = contractMapper.selectByOrderId(reqDTO.getOrderId());
+        if (existed != null) {
+            log.info("[createContractDraftFromSaleOrder] 閿�鍞鍗�({}) 宸茬敓鎴愬悎鍚�({})锛岃烦杩囧垱寤�",
+                    reqDTO.getOrderNo(), existed.getNo());
+            return existed;
+        }
+        // 2. 鏍¢獙瀹㈡埛涓庤礋璐d汉
+        CrmCustomerDO customer = customerService.getCustomer(reqDTO.getCustomerId());
+        if (customer == null) {
+            throw new IllegalArgumentException("閿�鍞鍗昜" + reqDTO.getOrderNo()
+                    + "]鍏宠仈鐨勫鎴蜂笉瀛樺湪鎴栧凡琚垹闄わ紝鏃犳硶鑷姩鐢熸垚閿�鍞悎鍚�");
+        }
+        if (reqDTO.getOwnerUserId() == null) {
+            throw new IllegalArgumentException("閿�鍞鍗昜" + reqDTO.getOrderNo()
+                    + "]鏈~鍐欓攢鍞憳锛屾棤娉曠‘瀹氳嚜鍔ㄧ敓鎴愰攢鍞悎鍚岀殑璐熻矗浜�");
+        }
+        if (CollUtil.isEmpty(reqDTO.getItems())) {
+            throw new IllegalArgumentException("閿�鍞鍗昜" + reqDTO.getOrderNo()
+                    + "]娌℃湁鏄庣粏锛屾棤娉曡嚜鍔ㄧ敓鎴愰攢鍞悎鍚�");
+        }
+
+        // 3. 缁勮鍚堝悓鏄庣粏
+        List<CrmContractProductDO> products = new ArrayList<>(reqDTO.getItems().size());
+        for (CrmContractCreateFromSaleOrderReqDTO.Item item : reqDTO.getItems()) {
+            CrmContractProductDO product = new CrmContractProductDO();
+            product.setItemId(item.getItemId());
+            product.setItemUnitId(item.getItemUnitId());
+            product.setItemPrice(item.getPrice());
+            product.setContractPrice(item.getPrice());
+            product.setCount(item.getCount());
+            product.setTotalPrice(MoneyUtils.priceMultiply(item.getPrice(), item.getCount()));
+            products.add(product);
+        }
+
+        // 4. 缁勮鍚堝悓锛堟湭鎻愪氦鑽夌锛屼笉鍙戣捣瀹℃壒锛�
+        CrmContractDO contract = new CrmContractDO();
+        contract.setNo(noRedisDAO.generate(CrmNoRedisDAO.CONTRACT_NO_PREFIX));
+        contract.setName(customer.getName() + "-閿�鍞悎鍚�-" + reqDTO.getOrderNo());
+        contract.setCustomerId(reqDTO.getCustomerId());
+        contract.setOwnerUserId(reqDTO.getOwnerUserId());
+        contract.setOrderDate(reqDTO.getOrderTime());
+        contract.setDiscountPercent(reqDTO.getDiscountPercent() != null ? reqDTO.getDiscountPercent() : BigDecimal.ZERO);
+        contract.setDepositPrice(reqDTO.getDepositPrice() != null ? reqDTO.getDepositPrice() : BigDecimal.ZERO);
+        contract.setOrderId(reqDTO.getOrderId());
+        contract.setOrderNo(reqDTO.getOrderNo());
+        contract.setRemark("鐢遍攢鍞鍗曘��" + reqDTO.getOrderNo() + "銆戝鎵归�氳繃鍚庤嚜鍔ㄧ敓鎴�");
+        contract.setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
+        calculateTotalPrice(contract, products);
+
+        // 5. 钀藉簱
+        contractMapper.insert(contract);
+        products.forEach(product -> product.setContractId(contract.getId()));
+        contractProductMapper.insertBatch(products);
+
+        // 6. 鍒涘缓鏁版嵁鏉冮檺锛屼繚璇佽礋璐d汉鑳界湅鍒拌鍚堝悓
+        crmPermissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(contract.getOwnerUserId())
+                .setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(contract.getId())
+                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
+        log.info("[createContractDraftFromSaleOrder] 閿�鍞鍗�({}) 鑷姩鐢熸垚鍚堝悓({}) 鎴愬姛",
+                reqDTO.getOrderNo(), contract.getNo());
+        return contract;
+    }
+
+    @Override
     public void updateContractStatus(Long id, Integer status) {
         contractMapper.updateById(new CrmContractDO()
                 .setId(id)

--
Gitblit v1.9.3