| | |
| | | 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; |
| | |
| | | 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; |
| | | |
| | |
| | | |
| | | @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<>()) |
| | |
| | | .failureList(new LinkedHashMap<>()) |
| | | .build(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | // 空文件也要给日志文案兜底,避免 {{#importSummary}} 取不到值 |
| | | OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList()); |
| | | return respVO; |
| | | } |
| | | |
| | |
| | | respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); |
| | | } |
| | | } |
| | | // 记录导入结果汇总,供 @LogRecord 的 success 文案引用 |
| | | OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList()); |
| | | return respVO; |
| | | } |
| | | |
| | |
| | | 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() |
| | | + "。请先在「CRM 客户管理」中创建该客户(名称需与模板完全一致),或修改模板中的客户名称后重新导入"); |
| | | } |
| | | 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() + "]的数量必须为正整数"); |
| | | 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. 组装主表(负责人=登录用户;状态=未提交) |
| | |
| | | contract.setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus()); |
| | | |
| | | // 5. 明细落库 + 自动算价 |
| | | List<CrmContractProductDO> productDOs = validateContractProducts(products); |
| | | calculateTotalPrice(contract, productDOs); |
| | | contractMapper.insert(contract); |
| | | productDOs.forEach(product -> product.setContractId(contract.getId())); |
| | |
| | | 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. 记录操作日志上下文 |
| | |
| | | } |
| | | |
| | | // 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 是否属于该分类且处于激活状态 |
| | |
| | | } |
| | | |
| | | @Override |
| | | public CrmContractDO getContractByNo(String no) { |
| | | return contractMapper.selectByNo(no); |
| | | } |
| | | |
| | | @Override |
| | | public CrmContractDO validateContract(Long id) { |
| | | return validateContractExists(id); |
| | | } |
| | |
| | | @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 |
| | |
| | | } |
| | | |
| | | @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. 校验客户与负责人 |
| | | 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. 创建数据权限,保证负责人能看到该合同 |
| | | 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) |