feat(crm): 完善投标保证金与报价单关联及作废功能
- 新增管理端撤销运行中流程实例接口
- 投标保证金新增关联报价单及双向绑定逻辑
- 增加投标中标时必须关联报价单的业务校验
- 销售报价单新增是否投标及关联保证金字段
- 新增报价单手动作废及轮次流水列表查询功能
- 新增投标保证金与报价单相关数据的导出接口
- 完善报价单列表中关联投标中标结果的展示
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- ===================================================== |
| | | -- æ¥ä»·ä¸ææ éç¯ |
| | | -- 1. crm_bid_bond å¢å å
³èæ¥ä»·ååæ®µï¼ææ ç»å®æ¥ä»·ï¼ |
| | | -- 2. crm_sale_quotation å¢å æ¯å¦éè¦ææ ãææ å
³èãæ¥ä»·è½®æ¬¡ãä½åºå段 |
| | | -- 3. æ°å»º crm_sale_quotation_revisionï¼æ¯è½®æ¥ä»·æµæ°´å¿«ç
§ï¼ |
| | | -- 4. èåï¼é宿¥ä»·-ä½åºãææ ä¿è¯é-å¯¼åº |
| | | -- å¹çï¼å¯é夿§è¡ï¼ä¸å« tenant_id |
| | | -- ===================================================== |
| | | |
| | | -- ---------- 1. crm_bid_bond ---------- |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_bid_bond' AND column_name = 'quotation_id'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_bid_bond` ADD COLUMN `quotation_id` BIGINT NULL COMMENT ''å
³èæ¥ä»·åID'' AFTER `tender_no`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_bid_bond' AND column_name = 'quotation_no'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_bid_bond` ADD COLUMN `quotation_no` VARCHAR(32) NULL COMMENT ''å
³èæ¥ä»·åå·å¿«ç
§'' AFTER `quotation_id`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_bid_bond' AND column_name = 'quotation_round'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_bid_bond` ADD COLUMN `quotation_round` INT NOT NULL DEFAULT 0 COMMENT ''å½åæ¥ä»·è½®æ¬¡'' AFTER `quotation_no`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.statistics |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_bid_bond' AND index_name = 'idx_quotation_id'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_bid_bond` ADD INDEX `idx_quotation_id` (`quotation_id`)', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | -- ---------- 2. crm_sale_quotation ---------- |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'need_bid'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `need_bid` TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''æ¯å¦éè¦ææ ï¼1æ¯ 0å¦'' AFTER `contract_no`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'bid_bond_id'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `bid_bond_id` BIGINT NULL COMMENT ''å
³èææ ï¼ä¿è¯éï¼ID'' AFTER `contract_no`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'bid_bond_no'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `bid_bond_no` VARCHAR(32) NULL COMMENT ''ææ åå·å¿«ç
§'' AFTER `bid_bond_id`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'revision_no'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `revision_no` INT NOT NULL DEFAULT 0 COMMENT ''å·²æäº¤æ¥ä»·è½®æ¬¡'' AFTER `bid_bond_no`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'void_type'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `void_type` TINYINT NULL COMMENT ''ä½åºç±»åï¼1æå¨ä½åº 2è¶
æä½åº'' AFTER `revision_no`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'void_reason'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `void_reason` VARCHAR(500) NULL COMMENT ''ä½åºåå '' AFTER `void_type`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'void_time'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `void_time` DATETIME NULL COMMENT ''ä½åºæ¶é´'' AFTER `void_reason`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.columns |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND column_name = 'void_by'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD COLUMN `void_by` BIGINT NULL COMMENT ''ä½åºäººç¨æ·ID'' AFTER `void_time`', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | SET @exists = (SELECT COUNT(*) FROM information_schema.statistics |
| | | WHERE table_schema = DATABASE() AND table_name = 'crm_sale_quotation' AND index_name = 'idx_bid_bond_id'); |
| | | SET @sql = IF(@exists = 0, |
| | | 'ALTER TABLE `crm_sale_quotation` ADD INDEX `idx_bid_bond_id` (`bid_bond_id`)', |
| | | 'SELECT 1'); |
| | | PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; |
| | | |
| | | -- ---------- 3. crm_sale_quotation_revision ---------- |
| | | CREATE TABLE IF NOT EXISTS `crm_sale_quotation_revision` ( |
| | | `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ç¼å·', |
| | | `quotation_id` BIGINT NOT NULL COMMENT 'æ¥ä»·åID', |
| | | `quotation_no` VARCHAR(32) NOT NULL COMMENT 'æ¥ä»·åå·å¿«ç
§', |
| | | `revision_no` INT NOT NULL COMMENT 'æ¥ä»·è½®æ¬¡', |
| | | `bid_bond_id` BIGINT DEFAULT NULL COMMENT '彿¶ç»å®çææ ID', |
| | | `bid_bond_no` VARCHAR(32) DEFAULT NULL COMMENT '彿¶ç»å®çææ åå·', |
| | | `customer_id` BIGINT DEFAULT NULL COMMENT '客æ·ID', |
| | | `name` VARCHAR(255) DEFAULT NULL COMMENT 'æ¥ä»·ååç§°å¿«ç
§', |
| | | `quotation_time` DATETIME DEFAULT NULL COMMENT 'æ¥ä»·æ¥æ', |
| | | `valid_until` DATETIME DEFAULT NULL COMMENT 'æææè³', |
| | | `total_product_price` DECIMAL(24,6) DEFAULT NULL COMMENT 'äº§åæ»éé¢', |
| | | `discount_percent` DECIMAL(24,6) DEFAULT NULL COMMENT 'æ´åææ£', |
| | | `discount_price` DECIMAL(24,6) DEFAULT NULL COMMENT '伿 éé¢', |
| | | `total_price` DECIMAL(24,6) DEFAULT NULL COMMENT 'æ¥ä»·æ»éé¢', |
| | | `tax_rate` DECIMAL(24,6) DEFAULT NULL COMMENT 'ç¨ç', |
| | | `tax_price` DECIMAL(24,6) DEFAULT NULL COMMENT 'ç¨é¢', |
| | | `items_snapshot` LONGTEXT COMMENT 'æç»JSONå¿«ç
§', |
| | | `status` TINYINT DEFAULT NULL COMMENT 'æäº¤æ¶ç¶æ', |
| | | `process_instance_id` VARCHAR(64) DEFAULT NULL COMMENT 'æµç¨å®ä¾ç¼å·', |
| | | `submit_user_id` BIGINT DEFAULT NULL COMMENT 'æäº¤äººç¨æ·ID', |
| | | `submit_time` DATETIME DEFAULT NULL COMMENT 'æäº¤æ¶é´', |
| | | `creator` VARCHAR(64) DEFAULT '' COMMENT 'å建è
', |
| | | `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', |
| | | `updater` VARCHAR(64) DEFAULT '' COMMENT 'æ´æ°è
', |
| | | `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', |
| | | `deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT 'æ¯å¦å é¤', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_quotation_revision` (`quotation_id`, `revision_no`), |
| | | KEY `idx_quotation_id` (`quotation_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CRMé宿¥ä»·è½®æ¬¡æµæ°´'; |
| | | |
| | | -- ---------- 4. èå ---------- |
| | | 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`) |
| | | VALUES |
| | | (602650, 'é宿¥ä»·-ä½åº', 'crm:sale-quotation:void', 3, 10, 602512, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'), |
| | | (602651, 'ææ ä¿è¯é-导åº', 'crm:bid-bond:export', 3, 9, 602630, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0') |
| | | ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `permission` = VALUES(`permission`), |
| | | `parent_id` = VALUES(`parent_id`), `sort` = VALUES(`sort`), |
| | | `status` = VALUES(`status`), `deleted` = b'0'; |
| | |
| | | */ |
| | | void cancelProcessInstanceByStartUser(Long userId, String processInstanceId, String reason); |
| | | |
| | | /** |
| | | * ç±ç®¡ç端æ¤éè¿è¡ä¸çæµç¨å®ä¾ï¼ä¸æ ¡éªå起人ï¼ã |
| | | * |
| | | * @param userId æä½äººç¨æ·ç¼å· |
| | | * @param processInstanceId æµç¨å®ä¾ç¼å· |
| | | * @param reason æ¤éåå |
| | | */ |
| | | void cancelProcessInstanceByAdmin(Long userId, String processInstanceId, String reason); |
| | | |
| | | |
| | | |
| | | } |
| | |
| | | processInstanceService.cancelProcessInstanceByStartUser(userId, reqVO); |
| | | } |
| | | |
| | | @Override |
| | | public void cancelProcessInstanceByAdmin(Long userId, String processInstanceId, String reason) { |
| | | BpmProcessInstanceCancelReqVO reqVO = new BpmProcessInstanceCancelReqVO(); |
| | | reqVO.setId(processInstanceId); |
| | | reqVO.setReason(reason); |
| | | processInstanceService.cancelProcessInstanceByAdmin(userId, reqVO); |
| | | } |
| | | |
| | | } |
| | |
| | | package cn.iocoder.yudao.module.crm.controller.admin.bidbond; |
| | | |
| | | import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageParam; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondPageReqVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondRespVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondSaleOrderRespVO; |
| | |
| | | import io.swagger.v3.oas.annotations.Parameter; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import jakarta.annotation.Resource; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import jakarta.validation.Valid; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.List; |
| | | |
| | | import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | |
| | | @Tag(name = "管çåå° - CRM ææ ä¿è¯é") |
| | |
| | | return success(bidBondService.getBidBondPage(pageReqVO)); |
| | | } |
| | | |
| | | @GetMapping("/export-excel") |
| | | @Operation(summary = "å¯¼åºææ ä¿è¯é Excel") |
| | | @PreAuthorize("@ss.hasPermission('crm:bid-bond:export')") |
| | | @ApiAccessLog(operateType = EXPORT) |
| | | public void exportBidBondExcel(@Valid CrmBidBondPageReqVO pageReqVO, |
| | | HttpServletResponse response) throws IOException { |
| | | pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); |
| | | List<CrmBidBondRespVO> list = bidBondService.getBidBondPage(pageReqVO).getList(); |
| | | ExcelUtils.write(response, "ææ ä¿è¯é.xls", "æ°æ®", CrmBidBondRespVO.class, list); |
| | | } |
| | | |
| | | @GetMapping("/sale-order-list") |
| | | @Operation(summary = "è·å客æ·ä¸å¯å
³èçéå®è®¢åï¼éå®å°è´¦ï¼") |
| | | @Parameter(name = "customerId", description = "客æ·ç¼å·", required = true) |
| | |
| | | @Schema(description = "å
³èéå®è®¢åå·", example = "XSDD202611050001") |
| | | private String saleOrderNo; |
| | | |
| | | @Schema(description = "å
³èæ¥ä»·åå·", example = "BJ202611050001") |
| | | private String quotationNo; |
| | | |
| | | @Schema(description = "䏿 ç»æï¼0 å¾
弿 ã1 已䏿 ã2 æªä¸æ ", example = "2") |
| | | private Integer bidResult; |
| | | |
| | |
| | | package cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo; |
| | | |
| | | import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
| | | import cn.idev.excel.annotation.ExcelProperty; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | |
| | | |
| | | @Schema(description = "管çåå° - CRM ææ ä¿è¯é Response VO") |
| | | @Data |
| | | @ExcelIgnoreUnannotated |
| | | public class CrmBidBondRespVO { |
| | | |
| | | @Schema(description = "ä¿è¯éè®°å½ç¼å·", example = "1024") |
| | | private Long id; |
| | | |
| | | @ExcelProperty("ä¿è¯éåå·") |
| | | @Schema(description = "ä¿è¯éåå·", example = "BZ202611050001") |
| | | private String no; |
| | | |
| | | @Schema(description = "客æ·ç¼å·ï¼ææ æ¹ï¼", example = "1024") |
| | | private Long customerId; |
| | | |
| | | @ExcelProperty("客æ·åç§°") |
| | | @Schema(description = "客æ·åç§°", example = "ææå»é¢") |
| | | private String customerName; |
| | | |
| | | @ExcelProperty("项ç®åç§°") |
| | | @Schema(description = "项ç®åç§°", example = "ææå»é¢è®¾å¤éè´é¡¹ç®") |
| | | private String projectName; |
| | | |
| | | @ExcelProperty("ææ ç¼å·") |
| | | @Schema(description = "ææ ç¼å·", example = "ZB-2026-001") |
| | | private String tenderNo; |
| | | |
| | | @Schema(description = "å
³èéå®è®¢åç¼å·ï¼éå®å°è´¦ï¼", example = "2048") |
| | | private Long saleOrderId; |
| | | |
| | | @ExcelProperty("å
³èéå®è®¢åå·") |
| | | @Schema(description = "å
³èéå®è®¢åå·", example = "XSDD202611050001") |
| | | private String saleOrderNo; |
| | | |
| | | @ExcelProperty("å
³èéå®è®¢åéé¢") |
| | | @Schema(description = "å
³èéå®è®¢åéé¢", example = "500000.00") |
| | | private BigDecimal saleOrderAmount; |
| | | |
| | | @Schema(description = "å
³èæ¥ä»·åç¼å·", example = "1024") |
| | | private Long quotationId; |
| | | |
| | | @ExcelProperty("å
³èæ¥ä»·åå·") |
| | | @Schema(description = "å
³èæ¥ä»·åå·", example = "BJ202611050001") |
| | | private String quotationNo; |
| | | |
| | | @ExcelProperty("æ¥ä»·è½®æ¬¡") |
| | | @Schema(description = "å
³èæ¥ä»·åå½å轮次", example = "2") |
| | | private Integer quotationRound; |
| | | |
| | | @Schema(description = "å
³èæ¥ä»·åéé¢", example = "100000.00") |
| | | private BigDecimal quotationTotalPrice; |
| | | |
| | | @Schema(description = "å
³èæ¥ä»·åç¶æï¼0 è稿ã1 审æ¹ä¸ã2 审æ¹éè¿ã3 审æ¹ä¸éè¿ã4 已忶ã5 å·²ä½åºã10 已转åå", example = "2") |
| | | private Integer quotationStatus; |
| | | |
| | | @ExcelProperty("ä¿è¯ééé¢") |
| | | @Schema(description = "ä¿è¯ééé¢", example = "50000.00") |
| | | private BigDecimal bondAmount; |
| | | |
| | | @ExcelProperty("ç¼´çº³æ¥æ") |
| | | @Schema(description = "ç¼´çº³æ¥æ", example = "2026-11-05") |
| | | private LocalDate payDate; |
| | | |
| | | @ExcelProperty("䏿 ç»æ") |
| | | @Schema(description = "䏿 ç»æï¼0 å¾
弿 ã1 已䏿 ã2 æªä¸æ ", example = "2") |
| | | private Integer bidResult; |
| | | |
| | | @ExcelProperty("éåç¶æ") |
| | | @Schema(description = "éåç¶æï¼0 æªéåã1 é¨åéåã2 å·²éåã3 æ ééå", example = "0") |
| | | private Integer refundStatus; |
| | | |
| | | @ExcelProperty("éåéé¢") |
| | | @Schema(description = "éåéé¢", example = "50000.00") |
| | | private BigDecimal refundAmount; |
| | | |
| | | @ExcelProperty("é忥æ") |
| | | @Schema(description = "é忥æ", example = "2026-12-01") |
| | | private LocalDate refundDate; |
| | | |
| | | @ExcelProperty("夿³¨") |
| | | @Schema(description = "夿³¨") |
| | | private String remark; |
| | | |
| | |
| | | @Schema(description = "å
³èéå®è®¢åIDï¼éå®å°è´¦ï¼", example = "2048") |
| | | private Long saleOrderId; |
| | | |
| | | @Schema(description = "å
³èæ¥ä»·åç¼å·", example = "1024") |
| | | private Long quotationId; |
| | | |
| | | @Schema(description = "ä¿è¯ééé¢", requiredMode = Schema.RequiredMode.REQUIRED, example = "50000.00") |
| | | @NotNull(message = "ä¿è¯ééé¢ä¸è½ä¸ºç©ºï¼è¯·å¡«åæ¬æ¬¡å客æ·ç¼´çº³çä¿è¯ééé¢") |
| | | private BigDecimal bondAmount; |
| | |
| | | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationOptionRespVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationRespVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationRevisionRespVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | 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.quotation.CrmSaleQuotationRevisionDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService; |
| | | import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService; |
| | | import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum; |
| | |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; |
| | | import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.UPDATE; |
| | | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; |
| | |
| | | |
| | | @Resource |
| | | private CrmSaleQuotationService saleQuotationService; |
| | | |
| | | @Resource |
| | | private CrmBidBondMapper bidBondMapper; |
| | | |
| | | @Resource |
| | | private AdminUserApi adminUserApi; |
| | |
| | | return success(saleQuotationService.convertToContract(id, getLoginUserId())); |
| | | } |
| | | |
| | | @PutMapping("/void") |
| | | @Operation(summary = "ä½åºé宿¥ä»·å") |
| | | @Parameter(name = "id", description = "æ¥ä»·åç¼å·", required = true) |
| | | @Parameter(name = "reason", description = "ä½åºåå ", required = true) |
| | | @PreAuthorize("@ss.hasPermission('crm:sale-quotation:void')") |
| | | @ApiAccessLog(operateType = UPDATE) |
| | | public CommonResult<Boolean> voidSaleQuotation(@RequestParam("id") Long id, |
| | | @RequestParam("reason") String reason) { |
| | | saleQuotationService.voidSaleQuotation(id, reason); |
| | | return success(true); |
| | | } |
| | | |
| | | @GetMapping("/revision/list") |
| | | @Operation(summary = "è·å¾æ¥ä»·åè½®æ¬¡æµæ°´å表") |
| | | @Parameter(name = "quotationId", description = "æ¥ä»·åç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')") |
| | | public CommonResult<List<CrmSaleQuotationRevisionRespVO>> getRevisionList( |
| | | @RequestParam("quotationId") Long quotationId) { |
| | | List<CrmSaleQuotationRevisionDO> list = saleQuotationService.getSaleQuotationRevisionList(quotationId); |
| | | return success(BeanUtils.toBean(list, CrmSaleQuotationRevisionRespVO.class)); |
| | | } |
| | | |
| | | @GetMapping("/revision/export-excel") |
| | | @Operation(summary = "å¯¼åºæ¥ä»·åè½®æ¬¡æµæ°´ Excel") |
| | | @Parameter(name = "quotationId", description = "æ¥ä»·åç¼å·", required = true) |
| | | @PreAuthorize("@ss.hasPermission('crm:sale-quotation:export')") |
| | | @ApiAccessLog(operateType = EXPORT) |
| | | public void exportRevisionExcel(@RequestParam("quotationId") Long quotationId, |
| | | HttpServletResponse response) throws IOException { |
| | | List<CrmSaleQuotationRevisionRespVO> list = BeanUtils.toBean( |
| | | saleQuotationService.getSaleQuotationRevisionList(quotationId), |
| | | CrmSaleQuotationRevisionRespVO.class); |
| | | ExcelUtils.write(response, "æ¥ä»·è½®æ¬¡æµæ°´.xls", "æ°æ®", CrmSaleQuotationRevisionRespVO.class, list); |
| | | } |
| | | |
| | | @GetMapping("/bindable-list") |
| | | @Operation(summary = "è·å¾å®¢æ·ä¸å¯ç»å®ææ çæ¥ä»·åå表") |
| | | @Parameter(name = "customerId", description = "客æ·ç¼å·", required = true) |
| | | @Parameter(name = "bidBondId", description = "å½åææ ä¿è¯éç¼å·ï¼ç¼è¾æ¶ç¨äºä¿çèªèº«ç»å®ï¼", required = false) |
| | | @PreAuthorize("@ss.hasPermission('crm:sale-quotation:query')") |
| | | public CommonResult<List<CrmSaleQuotationOptionRespVO>> getBindableList( |
| | | @RequestParam("customerId") Long customerId, |
| | | @RequestParam(value = "bidBondId", required = false) Long bidBondId) { |
| | | return success(BeanUtils.toBean( |
| | | saleQuotationService.getBindableQuotationList(customerId, bidBondId), |
| | | CrmSaleQuotationOptionRespVO.class)); |
| | | } |
| | | |
| | | // ==================== æå»ºååº VO ==================== |
| | | |
| | | private CrmSaleQuotationRespVO buildSaleQuotationRespVO(CrmSaleQuotationDO saleQuotation, List<CrmSaleQuotationProductDO> productList) { |
| | |
| | | } |
| | | // 3. å¡«å
å
³èä¿¡æ¯ |
| | | fillSaleQuotationRelatedFields(respVO); |
| | | // 3.1 å¡«å
å
³èææ ç䏿 ç»æ |
| | | if (saleQuotation.getBidBondId() != null) { |
| | | CrmBidBondDO bidBond = bidBondMapper.selectById(saleQuotation.getBidBondId()); |
| | | if (bidBond != null) { |
| | | respVO.setBidResult(bidBond.getBidResult()); |
| | | } |
| | | } |
| | | // 4. å¡«å
éä»¶å表 |
| | | respVO.setAttachmentList(storageAttachmentApi.listAttachments("crm_sale_quotation", saleQuotation.getId())); |
| | | return respVO; |
| | |
| | | // 1.6 åæºä¿¡æ¯ï¼è¿æ»¤ nullï¼businessId 为å¯éåæ®µï¼ |
| | | Map<Long, CrmBusinessDO> businessMap = businessService.getBusinessMap( |
| | | convertSet(pageResult.getList(), CrmSaleQuotationDO::getBusinessId, vo -> vo.getBusinessId() != null)); |
| | | // 1.7 ææ ä¿¡æ¯ï¼è¿æ»¤ nullï¼bidBondId 为å¯éåæ®µï¼ |
| | | Set<Long> bidBondIds = convertSet(pageResult.getList(), CrmSaleQuotationDO::getBidBondId, |
| | | vo -> vo.getBidBondId() != null); |
| | | Map<Long, CrmBidBondDO> bidBondMap = bidBondIds.isEmpty() ? Map.of() |
| | | : convertMap(bidBondMapper.selectByIds(bidBondIds), CrmBidBondDO::getId); |
| | | |
| | | // 2. å¼å§æ¼æ¥ |
| | | return BeanUtils.toBean(pageResult, CrmSaleQuotationRespVO.class, vo -> { |
| | |
| | | MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname())); |
| | | MapUtils.findAndThen(deptMap, vo.getOwnerUserId(), dept -> vo.setOwnerUserDeptName(dept.getName())); |
| | | MapUtils.findAndThen(businessMap, vo.getBusinessId(), business -> vo.setBusinessName(business.getName())); |
| | | MapUtils.findAndThen(bidBondMap, vo.getBidBondId(), bidBond -> vo.setBidResult(bidBond.getBidResult())); |
| | | }); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.controller.admin.quotation.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Schema(description = "管çåå° - CRM é宿¥ä»·å䏿é项 Response VO") |
| | | @Data |
| | | public class CrmSaleQuotationOptionRespVO { |
| | | |
| | | @Schema(description = "æ¥ä»·åç¼å·", example = "1024") |
| | | private Long id; |
| | | |
| | | @Schema(description = "æ¥ä»·åå·", example = "BJ202611050001") |
| | | private String no; |
| | | |
| | | @Schema(description = "æ¥ä»·ååç§°", example = "ææå»é¢è®¾å¤æ¥ä»·") |
| | | private String name; |
| | | |
| | | @Schema(description = "æ¥ä»·æ»éé¢", example = "100000.00") |
| | | private BigDecimal totalPrice; |
| | | |
| | | @Schema(description = "æ¥ä»·ç¶æï¼0 è稿ã2 审æ¹éè¿ã3 审æ¹ä¸éè¿", example = "2") |
| | | private Integer status; |
| | | |
| | | @Schema(description = "å·²æäº¤æ¥ä»·è½®æ¬¡", example = "2") |
| | | private Integer revisionNo; |
| | | |
| | | } |
| | |
| | | @InEnum(CrmSaleQuotationStatusEnum.class) |
| | | private Integer status; |
| | | |
| | | @Schema(description = "å
³èææ åå·", example = "BZ202611050001") |
| | | private String bidBondNo; |
| | | |
| | | } |
| | |
| | | @ExcelProperty("å
³èåååå·") |
| | | private String contractNo; |
| | | |
| | | @Schema(description = "æ¯å¦éè¦ææ ", example = "true") |
| | | private Boolean needBid; |
| | | |
| | | @Schema(description = "å
³èææ ä¿è¯éç¼å·", example = "2048") |
| | | private Long bidBondId; |
| | | @Schema(description = "å
³èææ ä¿è¯éåå·", example = "BZ202611050001") |
| | | @ExcelProperty("å
³èææ åå·") |
| | | private String bidBondNo; |
| | | @Schema(description = "å
³èææ ç䏿 ç»æï¼0 å¾
弿 ã1 已䏿 ã2 æªä¸æ ", example = "1") |
| | | private Integer bidResult; |
| | | |
| | | @Schema(description = "å·²æäº¤æ¥ä»·è½®æ¬¡", example = "2") |
| | | @ExcelProperty("æ¥ä»·è½®æ¬¡") |
| | | private Integer revisionNo; |
| | | |
| | | @Schema(description = "ä½åºç±»åï¼1 æå¨ä½åºã2 è¶
æä½åº", example = "1") |
| | | private Integer voidType; |
| | | @Schema(description = "ä½åºåå ") |
| | | @ExcelProperty("ä½åºåå ") |
| | | private String voidReason; |
| | | @Schema(description = "ä½åºæ¶é´") |
| | | @ExcelProperty("ä½åºæ¶é´") |
| | | private LocalDateTime voidTime; |
| | | @Schema(description = "ä½åºäººç¨æ·ç¼å·", example = "25682") |
| | | private Long voidBy; |
| | | @Schema(description = "ä½åºäººåå", example = "å¼ ä¸") |
| | | @ExcelProperty("ä½åºäºº") |
| | | private String voidByName; |
| | | |
| | | @Schema(description = "éä»¶å表") |
| | | private List<StorageBlobRespDTO> attachmentList; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.controller.admin.quotation.vo; |
| | | |
| | | import cn.idev.excel.annotation.ExcelIgnoreUnannotated; |
| | | import cn.idev.excel.annotation.ExcelProperty; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Schema(description = "管çåå° - CRM é宿¥ä»·è½®æ¬¡æµæ°´ Response VO") |
| | | @Data |
| | | @ExcelIgnoreUnannotated |
| | | public class CrmSaleQuotationRevisionRespVO { |
| | | |
| | | @Schema(description = "æµæ°´ç¼å·", example = "1024") |
| | | private Long id; |
| | | |
| | | @Schema(description = "æ¥ä»·åç¼å·", example = "1024") |
| | | private Long quotationId; |
| | | |
| | | @ExcelProperty("æ¥ä»·åå·") |
| | | @Schema(description = "æ¥ä»·åå·", example = "BJ202611050001") |
| | | private String quotationNo; |
| | | |
| | | @ExcelProperty("æ¥ä»·è½®æ¬¡") |
| | | @Schema(description = "æ¥ä»·è½®æ¬¡ï¼ä» 1 å¼å§", example = "2") |
| | | private Integer revisionNo; |
| | | |
| | | @ExcelProperty("å
³èææ åå·") |
| | | @Schema(description = "彿¶ç»å®çææ åå·", example = "BZ202611050001") |
| | | private String bidBondNo; |
| | | |
| | | @Schema(description = "客æ·ç¼å·", example = "1024") |
| | | private Long customerId; |
| | | |
| | | @ExcelProperty("æ¥ä»·ååç§°") |
| | | @Schema(description = "æ¥ä»·ååç§°å¿«ç
§") |
| | | private String name; |
| | | |
| | | @ExcelProperty("æ¥ä»·æ¥æ") |
| | | @Schema(description = "æ¥ä»·æ¥æ") |
| | | private LocalDateTime quotationTime; |
| | | |
| | | @ExcelProperty("æææè³") |
| | | @Schema(description = "æææè³") |
| | | private LocalDateTime validUntil; |
| | | |
| | | @ExcelProperty("äº§åæ»éé¢") |
| | | @Schema(description = "äº§åæ»éé¢", example = "19510") |
| | | private BigDecimal totalProductPrice; |
| | | |
| | | @ExcelProperty("æ´åææ£") |
| | | @Schema(description = "æ´åææ£", example = "10.00") |
| | | private BigDecimal discountPercent; |
| | | |
| | | @ExcelProperty("伿 éé¢") |
| | | @Schema(description = "伿 éé¢", example = "1951") |
| | | private BigDecimal discountPrice; |
| | | |
| | | @ExcelProperty("æ¥ä»·æ»éé¢") |
| | | @Schema(description = "æ¥ä»·æ»éé¢", example = "17559") |
| | | private BigDecimal totalPrice; |
| | | |
| | | @ExcelProperty("ç¨ç") |
| | | @Schema(description = "ç¨ç", example = "13.00") |
| | | private BigDecimal taxRate; |
| | | |
| | | @ExcelProperty("ç¨é¢") |
| | | @Schema(description = "ç¨é¢", example = "2282") |
| | | private BigDecimal taxPrice; |
| | | |
| | | @ExcelProperty("æäº¤äººç¼å·") |
| | | @Schema(description = "æäº¤äººç¨æ·ç¼å·", example = "25682") |
| | | private Long submitUserId; |
| | | |
| | | @ExcelProperty("æäº¤æ¶é´") |
| | | @Schema(description = "æäº¤æ¶é´") |
| | | private LocalDateTime submitTime; |
| | | |
| | | @Schema(description = "æç» JSON å¿«ç
§") |
| | | private String itemsSnapshot; |
| | | |
| | | @Schema(description = "æäº¤æ¶ç¶æ", example = "1") |
| | | private Integer status; |
| | | |
| | | @Schema(description = "æµç¨å®ä¾ç¼å·") |
| | | private String processInstanceId; |
| | | |
| | | } |
| | |
| | | @Schema(description = "ç¨ç", example = "13.00") |
| | | private BigDecimal taxRate; |
| | | |
| | | @Schema(description = "æ¯å¦éè¦ææ ï¼1éè¦ 0ä¸éè¦ï¼ä¸ä¼ é»è®¤éè¦", example = "true") |
| | | private Boolean needBid; |
| | | |
| | | @Schema(description = "éä»¶blobIdå表", example = "[1, 2, 3]") |
| | | private List<Long> blobIds; |
| | | |
| | |
| | | private String tenderNo; |
| | | |
| | | /** |
| | | * å
³èæ¥ä»·åç¼å· |
| | | */ |
| | | private Long quotationId; |
| | | |
| | | /** |
| | | * å
³èæ¥ä»·åå·å¿«ç
§ |
| | | */ |
| | | private String quotationNo; |
| | | |
| | | /** |
| | | * å½åæ¥ä»·è½®æ¬¡ |
| | | */ |
| | | private Integer quotationRound; |
| | | |
| | | /** |
| | | * å
³èéå®è®¢å IDï¼éå®å°è´¦ï¼ |
| | | */ |
| | | private Long saleOrderId; |
| | |
| | | * å
³èåååå· |
| | | */ |
| | | private String contractNo; |
| | | |
| | | /** |
| | | * æ¯å¦éè¦ææ |
| | | * |
| | | * éè¦ææ çæ¥ä»·åï¼å¿
é¡»ç»å®ææ 䏿æ 已䏿 åæè½è½¬åå |
| | | */ |
| | | private Boolean needBid; |
| | | |
| | | /** |
| | | * å
³èææ ï¼ä¿è¯éï¼ç¼å· |
| | | */ |
| | | private Long bidBondId; |
| | | /** |
| | | * å
³èææ åå·å¿«ç
§ |
| | | */ |
| | | private String bidBondNo; |
| | | /** |
| | | * å·²æäº¤æ¥ä»·è½®æ¬¡ |
| | | */ |
| | | private Integer revisionNo; |
| | | /** |
| | | * ä½åºç±»å |
| | | * |
| | | * æä¸¾ {@link cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationVoidTypeEnum} |
| | | */ |
| | | private Integer voidType; |
| | | /** |
| | | * ä½åºåå |
| | | */ |
| | | private String voidReason; |
| | | /** |
| | | * ä½åºæ¶é´ |
| | | */ |
| | | private LocalDateTime voidTime; |
| | | /** |
| | | * ä½åºäººç¨æ·ç¼å· |
| | | */ |
| | | private Long voidBy; |
| | | /** |
| | | * 夿³¨ |
| | | */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.dal.dataobject.quotation; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| | | import com.baomidou.mybatisplus.annotation.KeySequence; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.*; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * CRM é宿¥ä»·è½®æ¬¡æµæ°´ DO |
| | | * |
| | | * æ¯è½®æäº¤å®¡æ¹æ¶è®°å½ä¸ä»½æ¥ä»·å¿«ç
§ï¼ç¨äºäºæ¬¡/䏿¬¡æ¥ä»·çåå²è¿½æº¯ã |
| | | */ |
| | | @TableName("crm_sale_quotation_revision") |
| | | @KeySequence("crm_sale_quotation_revision_seq") |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | @ToString(callSuper = true) |
| | | @Builder |
| | | @NoArgsConstructor |
| | | @AllArgsConstructor |
| | | public class CrmSaleQuotationRevisionDO extends BaseDO { |
| | | |
| | | @TableId |
| | | private Long id; |
| | | /** |
| | | * æ¥ä»·åç¼å· |
| | | */ |
| | | private Long quotationId; |
| | | /** |
| | | * æ¥ä»·åå·å¿«ç
§ |
| | | */ |
| | | private String quotationNo; |
| | | /** |
| | | * æ¥ä»·è½®æ¬¡ï¼ä» 1 å¼å§ |
| | | */ |
| | | private Integer revisionNo; |
| | | /** |
| | | * 彿¶ç»å®çææ ç¼å· |
| | | */ |
| | | private Long bidBondId; |
| | | /** |
| | | * 彿¶ç»å®çææ åå· |
| | | */ |
| | | private String bidBondNo; |
| | | /** |
| | | * 客æ·ç¼å· |
| | | */ |
| | | private Long customerId; |
| | | /** |
| | | * æ¥ä»·ååç§°å¿«ç
§ |
| | | */ |
| | | private String name; |
| | | /** |
| | | * æ¥ä»·æ¥æ |
| | | */ |
| | | private LocalDateTime quotationTime; |
| | | /** |
| | | * æææè³ |
| | | */ |
| | | private LocalDateTime validUntil; |
| | | /** |
| | | * äº§åæ»éé¢ |
| | | */ |
| | | private BigDecimal totalProductPrice; |
| | | /** |
| | | * æ´åææ£ |
| | | */ |
| | | private BigDecimal discountPercent; |
| | | /** |
| | | * 伿 éé¢ |
| | | */ |
| | | private BigDecimal discountPrice; |
| | | /** |
| | | * æ¥ä»·æ»éé¢ |
| | | */ |
| | | private BigDecimal totalPrice; |
| | | /** |
| | | * ç¨ç |
| | | */ |
| | | private BigDecimal taxRate; |
| | | /** |
| | | * ç¨é¢ |
| | | */ |
| | | private BigDecimal taxPrice; |
| | | /** |
| | | * æç» JSON å¿«ç
§ |
| | | */ |
| | | private String itemsSnapshot; |
| | | /** |
| | | * æäº¤æ¶æ¥ä»·ç¶æ |
| | | */ |
| | | private Integer status; |
| | | /** |
| | | * æµç¨å®ä¾ç¼å· |
| | | */ |
| | | private String processInstanceId; |
| | | /** |
| | | * æäº¤äººç¨æ·ç¼å· |
| | | */ |
| | | private Long submitUserId; |
| | | /** |
| | | * æäº¤æ¶é´ |
| | | */ |
| | | private LocalDateTime submitTime; |
| | | |
| | | } |
| | |
| | | .eqIfPresent(CrmBidBondDO::getCustomerId, reqVO.getCustomerId()) |
| | | .likeIfPresent(CrmBidBondDO::getProjectName, reqVO.getProjectName()) |
| | | .likeIfPresent(CrmBidBondDO::getTenderNo, reqVO.getTenderNo()) |
| | | .likeIfPresent(CrmBidBondDO::getQuotationNo, reqVO.getQuotationNo()) |
| | | .likeIfPresent(CrmBidBondDO::getSaleOrderNo, reqVO.getSaleOrderNo()) |
| | | .eqIfPresent(CrmBidBondDO::getBidResult, reqVO.getBidResult()) |
| | | .eqIfPresent(CrmBidBondDO::getRefundStatus, reqVO.getRefundStatus()) |
| | |
| | | |
| | | default List<CrmBidBondDO> selectListByCustomerId(Long customerId) { |
| | | return selectList(CrmBidBondDO::getCustomerId, customerId); |
| | | } |
| | | |
| | | default CrmBidBondDO selectByQuotationId(Long quotationId) { |
| | | return selectOne(CrmBidBondDO::getQuotationId, quotationId); |
| | | } |
| | | |
| | | default List<CrmBidBondDO> selectListBySaleOrderIds(Collection<Long> saleOrderIds) { |
| | |
| | | .eq(CrmBidBondDO::getId, updateObj.getId()) |
| | | .set(CrmBidBondDO::getSaleOrderId, updateObj.getSaleOrderId()) |
| | | .set(CrmBidBondDO::getSaleOrderNo, updateObj.getSaleOrderNo()) |
| | | .set(CrmBidBondDO::getQuotationId, updateObj.getQuotationId()) |
| | | .set(CrmBidBondDO::getQuotationNo, updateObj.getQuotationNo()) |
| | | .set(CrmBidBondDO::getQuotationRound, updateObj.getQuotationRound()) |
| | | .set(CrmBidBondDO::getRefundAmount, updateObj.getRefundAmount()) |
| | | .set(CrmBidBondDO::getRefundDate, updateObj.getRefundDate())); |
| | | } |
| | |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Select; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | |
| | | /** |
| | |
| | | .eqIfPresent(CrmSaleQuotationDO::getCustomerId, pageReqVO.getCustomerId()) |
| | | .eqIfPresent(CrmSaleQuotationDO::getBusinessId, pageReqVO.getBusinessId()) |
| | | .eqIfPresent(CrmSaleQuotationDO::getStatus, pageReqVO.getStatus()) |
| | | .likeIfPresent(CrmSaleQuotationDO::getBidBondNo, pageReqVO.getBidBondNo()) |
| | | .orderByDesc(CrmSaleQuotationDO::getId); |
| | | |
| | | // å®¢æ·æéè¿æ»¤ |
| | |
| | | return selectList(CrmSaleQuotationDO::getCustomerId, customerId); |
| | | } |
| | | |
| | | default List<CrmSaleQuotationDO> selectListByCustomerIdAndStatuses(Long customerId, |
| | | Collection<Integer> statuses) { |
| | | return selectList(new LambdaQueryWrapperX<CrmSaleQuotationDO>() |
| | | .eq(CrmSaleQuotationDO::getCustomerId, customerId) |
| | | .in(CrmSaleQuotationDO::getStatus, statuses) |
| | | .orderByDesc(CrmSaleQuotationDO::getId)); |
| | | } |
| | | |
| | | default List<CrmSaleQuotationDO> selectListByBusinessId(Long businessId) { |
| | | return selectList(CrmSaleQuotationDO::getBusinessId, businessId); |
| | | } |
| | |
| | | .set(CrmSaleQuotationDO::getContractNo, null)); |
| | | } |
| | | |
| | | /** |
| | | * ç»å®ææ ï¼bidBondId 为 null æ¶è¡¨ç¤ºè§£ç» |
| | | */ |
| | | default void bindBidBond(Long quotationId, Long bidBondId, String bidBondNo) { |
| | | update(new LambdaUpdateWrapper<CrmSaleQuotationDO>() |
| | | .eq(CrmSaleQuotationDO::getId, quotationId) |
| | | .set(CrmSaleQuotationDO::getBidBondId, bidBondId) |
| | | .set(CrmSaleQuotationDO::getBidBondNo, bidBondNo)); |
| | | } |
| | | |
| | | /** |
| | | * éæ°æ¥ä»·ï¼é置为è稿并æ¸
空æµç¨å®ä¾ |
| | | */ |
| | | default void resetToDraft(Long quotationId) { |
| | | update(new LambdaUpdateWrapper<CrmSaleQuotationDO>() |
| | | .eq(CrmSaleQuotationDO::getId, quotationId) |
| | | .set(CrmSaleQuotationDO::getStatus, CrmSaleQuotationStatusEnum.DRAFT.getStatus()) |
| | | .set(CrmSaleQuotationDO::getProcessInstanceId, null)); |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.dal.mysql.quotation; |
| | | |
| | | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationRevisionDO; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Mapper |
| | | public interface CrmSaleQuotationRevisionMapper extends BaseMapperX<CrmSaleQuotationRevisionDO> { |
| | | |
| | | default List<CrmSaleQuotationRevisionDO> selectListByQuotationId(Long quotationId) { |
| | | return selectList(new LambdaQueryWrapperX<CrmSaleQuotationRevisionDO>() |
| | | .eq(CrmSaleQuotationRevisionDO::getQuotationId, quotationId) |
| | | .orderByAsc(CrmSaleQuotationRevisionDO::getRevisionNo)); |
| | | } |
| | | |
| | | default CrmSaleQuotationRevisionDO selectByQuotationIdAndRevisionNo(Long quotationId, Integer revisionNo) { |
| | | return selectOne(new LambdaQueryWrapperX<CrmSaleQuotationRevisionDO>() |
| | | .eq(CrmSaleQuotationRevisionDO::getQuotationId, quotationId) |
| | | .eq(CrmSaleQuotationRevisionDO::getRevisionNo, revisionNo)); |
| | | } |
| | | |
| | | } |
| | |
| | | ErrorCode SALE_QUOTATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_015_008, "é宿¥ä»·å({})ä¸å¤äºå®¡æ¹ä¸ï¼æ æ³æ´æ°å®¡æ¹ç»æ"); |
| | | ErrorCode SALE_QUOTATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_020_015_009, "é宿¥ä»·å®¡æ¹æµç¨æªé
ç½®ï¼è¯·å
卿µç¨ç®¡çä¸è®¾è®¡å¹¶å¯ç¨ãé宿¥ä»·å®¡æ¹ãæµç¨"); |
| | | ErrorCode SALE_QUOTATION_EXPORT_FAIL_NOT_APPROVE = new ErrorCode(1_020_015_010, "é宿¥ä»·å({})å½åç¶æä¸º[{}]ï¼åªæå®¡æ¹éè¿åæè½å¯¼åºå¸¦ç« æ¥ä»·å"); |
| | | ErrorCode SALE_QUOTATION_VOID_REASON_REQUIRED = new ErrorCode(1_020_015_011, "ä½åºåå ä¸è½ä¸ºç©ºï¼è¯·å¡«åä½åºè¯´æ"); |
| | | ErrorCode SALE_QUOTATION_VOID_FAIL_STATUS = new ErrorCode(1_020_015_012, "é宿¥ä»·å({})å½åç¶æä¸º[{}]ï¼ä¸è½ä½åº"); |
| | | ErrorCode SALE_QUOTATION_VOID_FAIL_WON = new ErrorCode(1_020_015_013, "é宿¥ä»·å({})已䏿 ï¼å¯ç»§ç»è½¬ååï¼ä¸è½ä½åºï¼å¦éç»æ¢è¯·å
å¤çææ ç»æ"); |
| | | ErrorCode SALE_QUOTATION_EXPIRED = new ErrorCode(1_020_015_014, "é宿¥ä»·å({})æææè³[{}]å·²è¿ï¼å·²èªå¨ä½åºï¼è¯·éæ°æ¥ä»·ååæäº¤"); |
| | | ErrorCode SALE_QUOTATION_REQUOTE_FAIL_NOT_LOST = new ErrorCode(1_020_015_015, "é宿¥ä»·å({})å½åç¶æä¸º[{}]ï¼åªæç»å®ææ 䏿æ ç»æä¸ºãæªä¸æ ãæ¶æå¯ä¿®æ¹åæ¥ä»·åéæ°æ¥ä»·"); |
| | | ErrorCode SALE_QUOTATION_REVISION_EXISTS = new ErrorCode(1_020_015_016, "é宿¥ä»·å({})第[{}]è½®æ¥ä»·è®°å½å·²åå¨ï¼è¯·å·æ°åéè¯"); |
| | | ErrorCode SALE_QUOTATION_CONVERT_FAIL_BID_NOT_WON = new ErrorCode(1_020_015_017, "é宿¥ä»·å({})å
³èçææ [{}]å½åç»æä¸º[{}]ï¼åªæææ 已䏿 åæè½è½¬åå"); |
| | | ErrorCode SALE_QUOTATION_CONVERT_FAIL_BID_QUOTATION_MISMATCH = new ErrorCode(1_020_015_018, "é宿¥ä»·å({})䏿¯ææ [{}]å½åç»å®çæ¥ä»·åï¼æ æ³è½¬ååï¼è¯·ä½¿ç¨ææ ææ°ç»å®çæ¥ä»·å"); |
| | | ErrorCode SALE_QUOTATION_VOIDED = new ErrorCode(1_020_015_019, "é宿¥ä»·å({})å·²ä½åºï¼åå ï¼[{}]ï¼ä¸è½ç»§ç»æä½ï¼å¦éç»§ç»è¯·éæ°æ¥ä»·"); |
| | | ErrorCode SALE_QUOTATION_DELETE_FAIL_BOUND = new ErrorCode(1_020_015_020, "é宿¥ä»·å({})å·²ç»å®ææ [{}]ï¼ä¸è½å é¤ï¼è¯·å
卿æ ä¿è¯éä¸è§£é¤å
³è"); |
| | | ErrorCode SALE_QUOTATION_CONVERT_FAIL_BID_REQUIRED = new ErrorCode(1_020_015_021, "é宿¥ä»·å({})æ 记为éè¦ææ ï¼ä½å°æªå
³èææ ï¼è¯·å
卿æ ä¿è¯éä¸ç»å®è¯¥æ¥ä»·åãå®æææ å¹¶ç»è®°ä¸æ ç»æåå转ååï¼æå°æ¥ä»·åæ¹ä¸ºä¸éè¦ææ "); |
| | | |
| | | // ========== å¼ç¥¨ 1_020_016_000 ========== |
| | | ErrorCode INVOICE_NOT_EXISTS = new ErrorCode(1_020_016_000, "å¼ç¥¨ä¸åå¨"); |
| | |
| | | ErrorCode BID_BOND_SALE_ORDER_NOT_APPROVE = new ErrorCode(1_020_018_012, "å
³èéå®è®¢å䏿¯å·²å®¡æ ¸ç¶æ"); |
| | | ErrorCode BID_BOND_SALE_ORDER_CUSTOMER_MISMATCH = new ErrorCode(1_020_018_013, "å
³èéå®è®¢åä¸ä¿è¯é客æ·ä¸ä¸è´"); |
| | | ErrorCode BID_BOND_LOST_CANNOT_LINK_ORDER = new ErrorCode(1_020_018_014, "æªä¸æ æ¶ä¸è½å
³èéå®è®¢å"); |
| | | ErrorCode BID_BOND_QUOTATION_NOT_EXISTS = new ErrorCode(1_020_018_015, "å
³èæ¥ä»·åä¸åå¨"); |
| | | ErrorCode BID_BOND_QUOTATION_CUSTOMER_MISMATCH = new ErrorCode(1_020_018_016, "å
³èæ¥ä»·åä¸ä¿è¯é客æ·ä¸ä¸è´"); |
| | | ErrorCode BID_BOND_QUOTATION_STATUS_INVALID = new ErrorCode(1_020_018_017, "å
³èæ¥ä»·åç¶æä¸å
许ç»å®ææ "); |
| | | ErrorCode BID_BOND_QUOTATION_ALREADY_BOUND = new ErrorCode(1_020_018_018, "å
³èæ¥ä»·åå·²ç»å®å
¶ä»ææ "); |
| | | ErrorCode BID_BOND_WON_QUOTATION_REQUIRED = new ErrorCode(1_020_018_019, "䏿 ç»æéæ©ã已䏿 ãæ¶å¿
é¡»å
³èæ¥ä»·å"); |
| | | ErrorCode BID_BOND_DELETE_FAIL_WON = new ErrorCode(1_020_018_020, "ææ ä¿è¯é已䏿 ä¸å
³èæ¥ä»·åï¼ä¸è½å é¤ï¼è¯·å
å¤çå
³èå
³ç³»æææ ç»æ"); |
| | | |
| | | ErrorCode CRM_SALES_TARGET_NOT_EXISTS = new ErrorCode(1_020_017_000, "éå®ç®æ ä¸åå¨"); |
| | | ErrorCode CRM_SALES_TARGET_PERIOD_VALUE_INVALID = new ErrorCode(1_020_017_001, "卿å¼ã{}ãæ ¼å¼ä¸æ£ç¡®ï¼åºä¸º {}"); |
| | |
| | | APPROVE(2, "审æ¹éè¿"), |
| | | REJECT(3, "审æ¹ä¸éè¿"), |
| | | CANCEL(4, "已忶"), |
| | | VOID(5, "å·²ä½åº"), |
| | | CONVERTED(10, "已转åå"); |
| | | |
| | | private final Integer status; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.enums.quotation; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.RequiredArgsConstructor; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | | * CRM é宿¥ä»·ä½åºç±»åæä¸¾ |
| | | */ |
| | | @RequiredArgsConstructor |
| | | @Getter |
| | | public enum CrmSaleQuotationVoidTypeEnum { |
| | | |
| | | MANUAL(1, "æå¨ä½åº"), |
| | | EXPIRED(2, "è¶
æä½åº"); |
| | | |
| | | private final Integer type; |
| | | private final String name; |
| | | |
| | | public static CrmSaleQuotationVoidTypeEnum valueOf(Integer type) { |
| | | return Arrays.stream(values()) |
| | | .filter(item -> item.getType().equals(type)) |
| | | .findFirst() |
| | | .orElse(null); |
| | | } |
| | | |
| | | } |
| | |
| | | import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondSaveReqVO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants; |
| | | import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidBondRefundStatusEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidResultEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum; |
| | | import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService; |
| | | import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi; |
| | | import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO; |
| | |
| | | @Resource |
| | | private CrmBidBondMapper bidBondMapper; |
| | | @Resource |
| | | private CrmSaleQuotationMapper saleQuotationMapper; |
| | | @Resource |
| | | private CrmCustomerService customerService; |
| | | @Resource |
| | | private ErpSaleOrderApi erpSaleOrderApi; |
| | |
| | | bond.setId(null); |
| | | bond.setNo(generateBondNo()); |
| | | bidBondMapper.insert(bond); |
| | | // å忥价åç»å®å
³ç³» |
| | | if (bond.getQuotationId() != null) { |
| | | saleQuotationMapper.bindBidBond(bond.getQuotationId(), bond.getId(), bond.getNo()); |
| | | } |
| | | return bond.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateBidBond(CrmBidBondSaveReqVO updateReqVO) { |
| | | validateBidBondExists(updateReqVO.getId()); |
| | | CrmBidBondDO oldBond = validateBidBondExists(updateReqVO.getId()); |
| | | CrmBidBondDO updateObj = validateAndBuild(updateReqVO); |
| | | updateObj.setId(updateReqVO.getId()); |
| | | // åå·ä¸å
许修æ¹ï¼ä¿æåå¼ |
| | | updateObj.setNo(null); |
| | | bidBondMapper.updateByIdIncludingNulls(updateObj); |
| | | // æ¥ä»·åååæ¶ï¼å
è§£ç»æ§æ¥ä»·åï¼åç»å®æ°æ¥ä»·å |
| | | if (oldBond.getQuotationId() != null && !oldBond.getQuotationId().equals(updateObj.getQuotationId())) { |
| | | saleQuotationMapper.bindBidBond(oldBond.getQuotationId(), null, null); |
| | | } |
| | | if (updateObj.getQuotationId() != null && !updateObj.getQuotationId().equals(oldBond.getQuotationId())) { |
| | | saleQuotationMapper.bindBidBond(updateObj.getQuotationId(), updateObj.getId(), oldBond.getNo()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteBidBond(Long id) { |
| | | validateBidBondExists(id); |
| | | CrmBidBondDO bond = validateBidBondExists(id); |
| | | if (CrmBidResultEnum.WON.getResult().equals(bond.getBidResult()) && bond.getQuotationId() != null) { |
| | | throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_BOND_DELETE_FAIL_WON); |
| | | } |
| | | // è§£é¤æ¥ä»·åç»å®ååå é¤ |
| | | if (bond.getQuotationId() != null) { |
| | | saleQuotationMapper.bindBidBond(bond.getQuotationId(), null, null); |
| | | } |
| | | bidBondMapper.deleteById(id); |
| | | } |
| | | |
| | |
| | | saleOrderNo = order.getNo(); |
| | | } |
| | | |
| | | // 5. éåç¶æä¸éé¢ |
| | | // 5. å
³èæ¥ä»·å |
| | | CrmSaleQuotationDO quotation = validateQuotation(reqVO, bidResult); |
| | | |
| | | // 6. éåç¶æä¸éé¢ |
| | | Integer refundStatus = resolveRefundStatus(reqVO.getRefundStatus(), bidResult); |
| | | validateRefund(reqVO, bondAmount, refundStatus); |
| | | |
| | |
| | | bond.setBidResult(bidResult); |
| | | bond.setRefundStatus(refundStatus); |
| | | bond.setSaleOrderNo(saleOrderNo); |
| | | bond.setQuotationNo(quotation != null ? quotation.getNo() : null); |
| | | bond.setQuotationRound(quotation != null && quotation.getRevisionNo() != null |
| | | ? quotation.getRevisionNo() : 0); |
| | | return bond; |
| | | } |
| | | |
| | | /** |
| | | * æ ¡éªå¹¶å è½½å
³èæ¥ä»·å |
| | | */ |
| | | private CrmSaleQuotationDO validateQuotation(CrmBidBondSaveReqVO reqVO, Integer bidResult) { |
| | | // 䏿 æ¶æ¥ä»·åå¿
å¡«ï¼ä¿è¯ä¸æ åå¯ä»¥æ®æ¤è½¬åå |
| | | if (CrmBidResultEnum.WON.getResult().equals(bidResult) && reqVO.getQuotationId() == null) { |
| | | throw new ServiceException(ErrorCodeConstants.BID_BOND_WON_QUOTATION_REQUIRED.getCode(), |
| | | "䏿 ç»æä¸ºã已䏿 ãæ¶å¿
é¡»å
³èæ¥ä»·åï¼å¦åæ æ³å该æ¥ä»·å转ååã" |
| | | + "è¯·éæ©æ¬æ¬¡ä¸æ çé宿¥ä»·ååéæ°ä¿åã"); |
| | | } |
| | | if (reqVO.getQuotationId() == null) { |
| | | return null; |
| | | } |
| | | CrmSaleQuotationDO quotation = saleQuotationMapper.selectById(reqVO.getQuotationId()); |
| | | if (quotation == null) { |
| | | throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_NOT_EXISTS.getCode(), |
| | | "å
³èæ¥ä»·å(ID=" + reqVO.getQuotationId() + ")ä¸åå¨ï¼è¯·å·æ°æ¥ä»·åå表åéæ°éæ©ã"); |
| | | } |
| | | if (!Objects.equals(reqVO.getCustomerId(), quotation.getCustomerId())) { |
| | | throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_CUSTOMER_MISMATCH.getCode(), |
| | | "å
³èæ¥ä»·å[" + quotation.getNo() + "]ç客æ·(ID=" + quotation.getCustomerId() |
| | | + ")ä¸ä¿è¯é客æ·(ID=" + reqVO.getCustomerId() |
| | | + ")ä¸ä¸è´ã请æ¹é该客æ·åä¸çæ¥ä»·åï¼æå
è°æ´ä¿è¯é客æ·ã"); |
| | | } |
| | | if (!Set.of(CrmSaleQuotationStatusEnum.DRAFT.getStatus(), |
| | | CrmSaleQuotationStatusEnum.APPROVE.getStatus(), |
| | | CrmSaleQuotationStatusEnum.REJECT.getStatus()).contains(quotation.getStatus())) { |
| | | throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_STATUS_INVALID.getCode(), |
| | | "å
³èæ¥ä»·å[" + quotation.getNo() + "]å½åç¶æä¸ºã" |
| | | + getQuotationStatusName(quotation.getStatus()) |
| | | + "ãï¼åªæãè稿ã审æ¹éè¿ã审æ¹ä¸éè¿ãçæ¥ä»·åå¯ä»¥ç»å®ææ ã" |
| | | + "请æ¹éå
¶ä»æ¥ä»·åï¼æå
éæ°æ¥ä»·ååç»å®ã"); |
| | | } |
| | | if (quotation.getBidBondId() != null && !Objects.equals(quotation.getBidBondId(), reqVO.getId())) { |
| | | CrmBidBondDO boundBond = bidBondMapper.selectById(quotation.getBidBondId()); |
| | | throw new ServiceException(ErrorCodeConstants.BID_BOND_QUOTATION_ALREADY_BOUND.getCode(), |
| | | "å
³èæ¥ä»·å[" + quotation.getNo() + "]å·²ç»å®ææ [" |
| | | + (boundBond != null ? boundBond.getNo() : "ID=" + quotation.getBidBondId()) |
| | | + "]ï¼å䏿¥ä»·ååªè½ç»å®ä¸å¼ ææ ã请æ¹éå
¶ä»æ¥ä»·åï¼æå
å¨è¯¥ææ ä¸è§£é¤å
³èã"); |
| | | } |
| | | return quotation; |
| | | } |
| | | |
| | | private Integer resolveRefundStatus(Integer refundStatus, Integer bidResult) { |
| | |
| | | Map<Long, CrmCustomerDO> customerMap = customerIds.isEmpty() ? Map.of() |
| | | : customerService.getCustomerMap(customerIds); |
| | | Map<Long, ErpSaleOrderRespDTO> orderMap = loadSaleOrderMap(bonds); |
| | | Map<Long, CrmSaleQuotationDO> quotationMap = loadQuotationMap(bonds); |
| | | |
| | | return bonds.stream().map(bond -> { |
| | | CrmBidBondRespVO respVO = BeanUtils.toBean(bond, CrmBidBondRespVO.class); |
| | |
| | | respVO.setSaleOrderNo(order.getNo()); |
| | | respVO.setSaleOrderAmount(order.getTotalPrice()); |
| | | } |
| | | CrmSaleQuotationDO quotation = bond.getQuotationId() == null ? null |
| | | : quotationMap.get(bond.getQuotationId()); |
| | | if (quotation != null) { |
| | | respVO.setQuotationNo(quotation.getNo()); |
| | | respVO.setQuotationStatus(quotation.getStatus()); |
| | | respVO.setQuotationTotalPrice(quotation.getTotalPrice()); |
| | | } |
| | | return respVO; |
| | | }).collect(Collectors.toList()); |
| | | } |
| | | |
| | | private Map<Long, CrmSaleQuotationDO> loadQuotationMap(List<CrmBidBondDO> bonds) { |
| | | Set<Long> quotationIds = bonds.stream().map(CrmBidBondDO::getQuotationId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | if (quotationIds.isEmpty()) { |
| | | return Map.of(); |
| | | } |
| | | return saleQuotationMapper.selectByIds(quotationIds).stream() |
| | | .collect(Collectors.toMap(CrmSaleQuotationDO::getId, q -> q, (a, b) -> a)); |
| | | } |
| | | |
| | | /** |
| | |
| | | return orderMap; |
| | | } |
| | | |
| | | private String getQuotationStatusName(Integer status) { |
| | | CrmSaleQuotationStatusEnum statusEnum = CrmSaleQuotationStatusEnum.getByStatus(status); |
| | | return statusEnum != null ? statusEnum.getName() : "æªç¥ç¶æ(" + status + ")"; |
| | | } |
| | | |
| | | private String getSaleOrderStatusName(Integer status) { |
| | | if (status == null) { |
| | | return "æªç¥"; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.service.quotation; |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidResultEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationVoidTypeEnum; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.LocalTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | |
| | | /** |
| | | * é宿¥ä»·æææå¤å®ä¸å¨æä½åº |
| | | * |
| | | * æ¥è¯¢/æä½æ¥ä»·åæ¶æ validUntil 卿夿æ¯å¦è¿æï¼è¿æåè½åºä¸ºãå·²ä½åºãï¼ |
| | | * ä¿è¯å表ã详æ
ä¸åç»æä½è¡ä¸ºä¸è´ï¼ä¸ä¾èµå®æ¶ä»»å¡ã |
| | | */ |
| | | @Component |
| | | @Slf4j |
| | | public class CrmSaleQuotationExpiryHelper { |
| | | |
| | | private static final DateTimeFormatter REASON_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); |
| | | private static final Set<Integer> EXPIRABLE_STATUSES = Set.of( |
| | | CrmSaleQuotationStatusEnum.DRAFT.getStatus(), |
| | | CrmSaleQuotationStatusEnum.PROCESS.getStatus(), |
| | | CrmSaleQuotationStatusEnum.APPROVE.getStatus(), |
| | | CrmSaleQuotationStatusEnum.REJECT.getStatus()); |
| | | |
| | | @Resource |
| | | private CrmSaleQuotationMapper saleQuotationMapper; |
| | | @Resource |
| | | private CrmBidBondMapper bidBondMapper; |
| | | @Resource |
| | | private BpmProcessInstanceApi bpmProcessInstanceApi; |
| | | |
| | | /** |
| | | * æ¯å¦å·²è¿æææã |
| | | * |
| | | * åç«¯æ¥ææ§ä»¶åªéå°æ¥ï¼æ¶é´é¨å为 00:00:00ï¼ï¼æ¤æ¶æãå½å¤©ç»æå仿æãå¤çã |
| | | */ |
| | | public boolean isExpired(LocalDateTime validUntil) { |
| | | if (validUntil == null) { |
| | | return false; |
| | | } |
| | | if (LocalTime.MIDNIGHT.equals(validUntil.toLocalTime())) { |
| | | return LocalDate.now().isAfter(validUntil.toLocalDate()); |
| | | } |
| | | return LocalDateTime.now().isAfter(validUntil); |
| | | } |
| | | |
| | | public void expireIfNeeded(CrmSaleQuotationDO quotation) { |
| | | if (quotation == null) { |
| | | return; |
| | | } |
| | | expireIfNeeded(List.of(quotation)); |
| | | } |
| | | |
| | | public void expireIfNeeded(List<CrmSaleQuotationDO> quotations) { |
| | | if (CollUtil.isEmpty(quotations)) { |
| | | return; |
| | | } |
| | | List<CrmSaleQuotationDO> candidates = quotations.stream() |
| | | .filter(Objects::nonNull) |
| | | .filter(q -> EXPIRABLE_STATUSES.contains(q.getStatus())) |
| | | .filter(q -> isExpired(q.getValidUntil())) |
| | | .collect(Collectors.toList()); |
| | | if (candidates.isEmpty()) { |
| | | return; |
| | | } |
| | | Map<Long, CrmBidBondDO> bondMap = loadBondMap(candidates); |
| | | Long userId = getLoginUserId(); |
| | | for (CrmSaleQuotationDO quotation : candidates) { |
| | | if (isBoundBidInProgress(quotation, bondMap)) { |
| | | continue; |
| | | } |
| | | doExpire(quotation, userId); |
| | | } |
| | | } |
| | | |
| | | private Map<Long, CrmBidBondDO> loadBondMap(List<CrmSaleQuotationDO> quotations) { |
| | | Set<Long> bondIds = convertSet(quotations, CrmSaleQuotationDO::getBidBondId, Objects::nonNull); |
| | | if (bondIds.isEmpty()) { |
| | | return Map.of(); |
| | | } |
| | | return bidBondMapper.selectByIds(bondIds).stream() |
| | | .collect(Collectors.toMap(CrmBidBondDO::getId, Function.identity(), (a, b) -> a)); |
| | | } |
| | | |
| | | /** |
| | | * ææ è¿è¡ä¸ï¼å¾
弿 /已䏿 ï¼çæ¥ä»·åä¸èªå¨ä½åºï¼é¿å
æ£å¨å䏿æ çæ¥ä»·å¤±æã |
| | | */ |
| | | private boolean isBoundBidInProgress(CrmSaleQuotationDO quotation, Map<Long, CrmBidBondDO> bondMap) { |
| | | if (quotation.getBidBondId() == null) { |
| | | return false; |
| | | } |
| | | CrmBidBondDO bond = bondMap.get(quotation.getBidBondId()); |
| | | if (bond == null) { |
| | | return false; |
| | | } |
| | | return CrmBidResultEnum.PENDING.getResult().equals(bond.getBidResult()) |
| | | || CrmBidResultEnum.WON.getResult().equals(bond.getBidResult()); |
| | | } |
| | | |
| | | private void doExpire(CrmSaleQuotationDO quotation, Long userId) { |
| | | String reason = "æ¥ä»·å[" + quotation.getNo() + "]æææè³[" |
| | | + quotation.getValidUntil().format(REASON_TIME_FORMATTER) + "]å·²è¿ï¼ç³»ç»èªå¨ä½åº"; |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | boolean wasProcessing = CrmSaleQuotationStatusEnum.PROCESS.getStatus().equals(quotation.getStatus()); |
| | | // å
è½åºä¸ºå·²ä½åºï¼BPM æ¤éåè°åªä¼å¤çã审æ¹ä¸ãç¶æï¼ä¸ä¼è¦çæ¬ç¶æ |
| | | saleQuotationMapper.updateById(new CrmSaleQuotationDO() |
| | | .setId(quotation.getId()) |
| | | .setStatus(CrmSaleQuotationStatusEnum.VOID.getStatus()) |
| | | .setVoidType(CrmSaleQuotationVoidTypeEnum.EXPIRED.getType()) |
| | | .setVoidReason(reason) |
| | | .setVoidTime(now)); |
| | | quotation.setStatus(CrmSaleQuotationStatusEnum.VOID.getStatus()); |
| | | quotation.setVoidType(CrmSaleQuotationVoidTypeEnum.EXPIRED.getType()); |
| | | quotation.setVoidReason(reason); |
| | | quotation.setVoidTime(now); |
| | | // 审æ¹ä¸çæ¥ä»·åéè¦åæ¥æ¤éæµç¨ï¼é¿å
审æ¹äººç»§ç»å¤ç已失æçæ¥ä»· |
| | | if (wasProcessing) { |
| | | cancelProcessQuietly(quotation.getProcessInstanceId(), reason, userId); |
| | | } |
| | | } |
| | | |
| | | private void cancelProcessQuietly(String processInstanceId, String reason, Long userId) { |
| | | if (processInstanceId == null || processInstanceId.isBlank()) { |
| | | return; |
| | | } |
| | | try { |
| | | bpmProcessInstanceApi.cancelProcessInstanceByAdmin(userId, processInstanceId, reason); |
| | | } catch (Exception ex) { |
| | | log.warn("[cancelProcessQuietly][æ¥ä»·åæµç¨({})æ¤é失败ï¼ä¸å½±åèªå¨ä½åº]", processInstanceId, ex); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO; |
| | | 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.quotation.CrmSaleQuotationRevisionDO; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | |
| | | * @param quotationId æ¥ä»·åç¼å· |
| | | */ |
| | | void clearContractReference(Long quotationId); |
| | | |
| | | /** |
| | | * æå¨ä½åºæ¥ä»·å |
| | | * |
| | | * @param id æ¥ä»·åç¼å· |
| | | * @param reason ä½åºåå |
| | | */ |
| | | void voidSaleQuotation(Long id, String reason); |
| | | |
| | | /** |
| | | * è·å¾æ¥ä»·åçè½®æ¬¡æµæ°´å表 |
| | | * |
| | | * @param quotationId æ¥ä»·åç¼å· |
| | | * @return è½®æ¬¡æµæ°´å表 |
| | | */ |
| | | List<CrmSaleQuotationRevisionDO> getSaleQuotationRevisionList(Long quotationId); |
| | | |
| | | /** |
| | | * è·å¾å®¢æ·ä¸å¯ç»å®ææ çæ¥ä»·åå表ï¼è稿/审æ¹éè¿/审æ¹ä¸éè¿ï¼ä¸æªè¢«å
¶ä»ææ ç»å®ï¼ |
| | | * |
| | | * @param customerId 客æ·ç¼å· |
| | | * @param bidBondId å½åææ ç¼å·ï¼ç¼è¾æ¶ç¨äºä¿çèªèº«å·²ç»å®çæ¥ä»·åï¼ |
| | | * @return æ¥ä»·åå表 |
| | | */ |
| | | List<CrmSaleQuotationDO> getBindableQuotationList(Long customerId, Long bidBondId); |
| | | } |
| | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| | | import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | |
| | | import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractSaveReqVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationPageReqVO; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.quotation.vo.CrmSaleQuotationSaveReqVO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO; |
| | | 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.quotation.CrmSaleQuotationRevisionDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationProductMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationRevisionMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO; |
| | | import cn.iocoder.yudao.module.crm.enums.bidbond.CrmBidResultEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationStatusEnum; |
| | | import cn.iocoder.yudao.module.crm.enums.quotation.CrmSaleQuotationVoidTypeEnum; |
| | | import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService; |
| | | import cn.iocoder.yudao.module.crm.service.contact.CrmContactService; |
| | | import cn.iocoder.yudao.module.crm.service.contract.CrmContractService; |
| | |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | | import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.flowable.engine.repository.ProcessDefinition; |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | 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.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | |
| | | */ |
| | | private static final String SALE_QUOTATION_APPROVE_CATEGORY_CODE = "sale_quotation_approve"; |
| | | |
| | | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | |
| | | @Resource |
| | | private CrmSaleQuotationMapper saleQuotationMapper; |
| | | @Resource |
| | | private CrmSaleQuotationProductMapper saleQuotationProductMapper; |
| | | @Resource |
| | | private CrmSaleQuotationRevisionMapper revisionMapper; |
| | | @Resource |
| | | private CrmBidBondMapper bidBondMapper; |
| | | @Resource |
| | | private CrmSaleQuotationExpiryHelper expiryHelper; |
| | | |
| | | @Resource |
| | | private CrmNoRedisDAO noRedisDAO; |
| | |
| | | // 2.1 æå
¥æ¥ä»·å |
| | | CrmSaleQuotationDO saleQuotation = BeanUtils.toBean(createReqVO, CrmSaleQuotationDO.class, q -> q |
| | | .setNo(no) |
| | | .setNeedBid(!Boolean.FALSE.equals(createReqVO.getNeedBid())) |
| | | .setStatus(CrmSaleQuotationStatusEnum.DRAFT.getStatus())); |
| | | calculateTotalPrice(saleQuotation, quotationProducts); |
| | | saleQuotationMapper.insert(saleQuotation); |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateSaleQuotation(CrmSaleQuotationSaveReqVO updateReqVO) { |
| | | // 1.1 æ ¡éªåå¨ |
| | | // 1.1 æ ¡éªåå¨ï¼å¹¶å¤çè¿æ |
| | | CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(updateReqVO.getId()); |
| | | // 1.2 æ ¡éªç¶æï¼åªæèç¨¿ç¶ææè½ä¿®æ¹ |
| | | expiryHelper.expireIfNeeded(saleQuotation); |
| | | validateNotVoid(saleQuotation); |
| | | // 1.2 æ ¡éªç¶æï¼è稿å¯ç´æ¥ä¿®æ¹ï¼ç»å®ææ ä¸ææ æªä¸æ æ¶ï¼å
许修æ¹å·²å®¡æ¹çæ¥ä»·åéæ°æ¥ä»· |
| | | boolean requote = false; |
| | | if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus())) { |
| | | throw exception(SALE_QUOTATION_UPDATE_FAIL_NOT_DRAFT, saleQuotation.getNo()); |
| | | requote = canRequote(saleQuotation); |
| | | if (!requote) { |
| | | throw exception(SALE_QUOTATION_REQUOTE_FAIL_NOT_LOST, saleQuotation.getNo(), |
| | | getStatusName(saleQuotation.getStatus())); |
| | | } |
| | | } |
| | | // 1.3 æ ¡éªç©æé¡¹çæææ§ |
| | | List<CrmSaleQuotationProductDO> quotationProducts = validateQuotationItems(updateReqVO.getItems()); |
| | |
| | | } |
| | | calculateTotalPrice(updateObj, quotationProducts); |
| | | saleQuotationMapper.updateById(updateObj); |
| | | // 2.2 æ´æ°æ¥ä»·æç» |
| | | // 2.2 éæ°æ¥ä»·ï¼é置为è稿并æ¸
空ä¸ä¸è½®æµç¨å®ä¾ï¼çå¾
éæ°æäº¤ |
| | | if (requote) { |
| | | saleQuotationMapper.resetToDraft(saleQuotation.getId()); |
| | | } |
| | | // 2.3 æ´æ°æ¥ä»·æç» |
| | | updateQuotationProductList(updateReqVO.getId(), quotationProducts); |
| | | storageAttachmentApi.updateAttachments("file", "crm_sale_quotation", updateReqVO.getId(), updateReqVO.getBlobIds()); |
| | | } |
| | |
| | | saleQuotations.forEach(saleQuotation -> { |
| | | if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus())) { |
| | | throw exception(SALE_QUOTATION_DELETE_FAIL_NOT_DRAFT, saleQuotation.getNo()); |
| | | } |
| | | if (saleQuotation.getBidBondId() != null) { |
| | | throw exception(SALE_QUOTATION_DELETE_FAIL_BOUND, saleQuotation.getNo(), saleQuotation.getBidBondNo()); |
| | | } |
| | | }); |
| | | |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitSaleQuotation(Long id, String processDefinitionKey, Long userId) { |
| | | // 1. æ ¡éªæ¥ä»·ååå¨ä¸ä¸ºèç¨¿ç¶æ |
| | | // 1. æ ¡éªæ¥ä»·ååå¨ä¸ä¸ºèç¨¿ç¶æï¼å
å¤çè¿æï¼é¿å
æäº¤å·²å¤±æçæ¥ä»·ï¼ |
| | | CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id); |
| | | expiryHelper.expireIfNeeded(saleQuotation); |
| | | validateNotVoid(saleQuotation); |
| | | if (ObjUtil.notEqual(saleQuotation.getStatus(), CrmSaleQuotationStatusEnum.DRAFT.getStatus())) { |
| | | throw exception(SALE_QUOTATION_SUBMIT_FAIL_NOT_DRAFT, saleQuotation.getNo()); |
| | | } |
| | | // 1.1 è®°å½æ¬è½®æ¥ä»·æµæ°´ï¼å«æ¥ä»·å¿«ç
§ä¸æç» JSONï¼ |
| | | recordRevision(saleQuotation, userId); |
| | | |
| | | // 2. å
¨å±å
审æ¹ï¼ç´æ¥ç½®ä¸ºå®¡æ¹éè¿ï¼æ éæµç¨æ¨¡å |
| | | if (bpmProcessInstanceApi.isGlobalAutoApprove()) { |
| | |
| | | |
| | | @Override |
| | | public CrmSaleQuotationDO getSaleQuotation(Long id) { |
| | | return saleQuotationMapper.selectById(id); |
| | | CrmSaleQuotationDO quotation = saleQuotationMapper.selectById(id); |
| | | expiryHelper.expireIfNeeded(quotation); |
| | | return quotation; |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<CrmSaleQuotationDO> getSaleQuotationPage(CrmSaleQuotationPageReqVO pageReqVO, Long userId) { |
| | | // è·åç¨æ·ææéçå®¢æ· ID å表 |
| | | List<Long> permittedCustomerIds = customerApi.getPermittedCustomerIds(); |
| | | return saleQuotationMapper.selectPage(pageReqVO, userId, permittedCustomerIds); |
| | | PageResult<CrmSaleQuotationDO> pageResult = saleQuotationMapper.selectPage(pageReqVO, userId, permittedCustomerIds); |
| | | expiryHelper.expireIfNeeded(pageResult.getList()); |
| | | return pageResult; |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<CrmSaleQuotationDO> getSaleQuotationPageByCustomerId(CrmSaleQuotationPageReqVO pageReqVO) { |
| | | return saleQuotationMapper.selectPageByCustomerId(pageReqVO); |
| | | PageResult<CrmSaleQuotationDO> pageResult = saleQuotationMapper.selectPageByCustomerId(pageReqVO); |
| | | expiryHelper.expireIfNeeded(pageResult.getList()); |
| | | return pageResult; |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<CrmSaleQuotationDO> getSaleQuotationPageByBusinessId(CrmSaleQuotationPageReqVO pageReqVO) { |
| | | return saleQuotationMapper.selectPageByBusinessId(pageReqVO); |
| | | PageResult<CrmSaleQuotationDO> pageResult = saleQuotationMapper.selectPageByBusinessId(pageReqVO); |
| | | expiryHelper.expireIfNeeded(pageResult.getList()); |
| | | return pageResult; |
| | | } |
| | | |
| | | @Override |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long convertToContract(Long id, Long userId) { |
| | | // 1. æ ¡éªæ¥ä»·ååå¨ï¼ä¸ä¸ºè稿æå®¡æ¹éè¿ç¶æ |
| | | // 1. æ ¡éªæ¥ä»·ååå¨ï¼å¹¶å¤çè¿æ |
| | | CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id); |
| | | if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus()) |
| | | && !CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus())) { |
| | | CrmSaleQuotationStatusEnum current = CrmSaleQuotationStatusEnum.getByStatus(saleQuotation.getStatus()); |
| | | throw exception(SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT, saleQuotation.getNo(), |
| | | current != null ? current.getName() : String.valueOf(saleQuotation.getStatus())); |
| | | expiryHelper.expireIfNeeded(saleQuotation); |
| | | validateNotVoid(saleQuotation); |
| | | // 1.1 éè¦ææ çæ¥ä»·åï¼å¿
é¡»å
ç»å®ææ |
| | | boolean needBid = !Boolean.FALSE.equals(saleQuotation.getNeedBid()); |
| | | if (needBid && saleQuotation.getBidBondId() == null) { |
| | | throw exception(SALE_QUOTATION_CONVERT_FAIL_BID_REQUIRED, saleQuotation.getNo()); |
| | | } |
| | | // æ ¡éªæ¯å¦å·²è½¬åå |
| | | // 1.2 ç»å®ææ çæ¥ä»·åï¼å¿
须已䏿 ï¼ä¸æ¯è¯¥ææ å½åç»å®çæ¥ä»·å |
| | | if (saleQuotation.getBidBondId() != null) { |
| | | CrmBidBondDO bidBond = bidBondMapper.selectById(saleQuotation.getBidBondId()); |
| | | if (bidBond == null) { |
| | | throw new ServiceException(BID_BOND_NOT_EXISTS.getCode(), |
| | | "é宿¥ä»·å[" + saleQuotation.getNo() + "]å
³èçææ (ID=" + saleQuotation.getBidBondId() |
| | | + ")ä¸åå¨ï¼æ æ³è½¬ååï¼è¯·å
卿æ ä¿è¯éä¸éæ°å
³èææ ï¼æè§£é¤æ¥ä»·åç»å®ã"); |
| | | } |
| | | if (!CrmBidResultEnum.WON.getResult().equals(bidBond.getBidResult())) { |
| | | throw exception(SALE_QUOTATION_CONVERT_FAIL_BID_NOT_WON, saleQuotation.getNo(), |
| | | bidBond.getNo(), getBidResultName(bidBond.getBidResult())); |
| | | } |
| | | if (!saleQuotation.getId().equals(bidBond.getQuotationId())) { |
| | | throw exception(SALE_QUOTATION_CONVERT_FAIL_BID_QUOTATION_MISMATCH, |
| | | saleQuotation.getNo(), bidBond.getNo()); |
| | | } |
| | | if (!CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus())) { |
| | | throw exception(SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT, saleQuotation.getNo(), |
| | | getStatusName(saleQuotation.getStatus())); |
| | | } |
| | | } else if (!CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus()) |
| | | && !CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus())) { |
| | | throw exception(SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT, saleQuotation.getNo(), |
| | | getStatusName(saleQuotation.getStatus())); |
| | | } |
| | | // 1.2 æ ¡éªæ¯å¦å·²è½¬åå |
| | | if (saleQuotation.getContractId() != null) { |
| | | throw exception(SALE_QUOTATION_CONTRACT_EXISTS); |
| | | } |
| | |
| | | saleQuotationMapper.clearContractReference(quotationId); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void voidSaleQuotation(Long id, String reason) { |
| | | // 1.1 æ ¡éªä½åºåå |
| | | String trimmedReason = StrUtil.trim(reason); |
| | | if (StrUtil.isBlank(trimmedReason)) { |
| | | throw exception(SALE_QUOTATION_VOID_REASON_REQUIRED); |
| | | } |
| | | if (trimmedReason.length() > 500) { |
| | | throw new ServiceException(SALE_QUOTATION_VOID_REASON_REQUIRED.getCode(), |
| | | "ä½åºåå ä¸è½è¶
è¿ 500 个å符ï¼å½å为 " + trimmedReason.length() + " 个å符ï¼è¯·ç²¾ç®åéè¯"); |
| | | } |
| | | // 1.2 æ ¡éªæ¥ä»·ååå¨ï¼å¹¶å¤çè¿æï¼å·²è¿æåæè¶
æä½åºæç¤ºï¼ä¸åèµ°æå¨ä½åºï¼ |
| | | CrmSaleQuotationDO saleQuotation = validateSaleQuotationExists(id); |
| | | expiryHelper.expireIfNeeded(saleQuotation); |
| | | validateNotVoid(saleQuotation); |
| | | // 1.3 已䏿 çæ¥ä»·åä¸å
许ä½åºï¼å¦åååéç¯æè£ |
| | | if (saleQuotation.getBidBondId() != null) { |
| | | CrmBidBondDO bidBond = bidBondMapper.selectById(saleQuotation.getBidBondId()); |
| | | if (bidBond != null && CrmBidResultEnum.WON.getResult().equals(bidBond.getBidResult())) { |
| | | throw exception(SALE_QUOTATION_VOID_FAIL_WON, saleQuotation.getNo()); |
| | | } |
| | | } |
| | | // 1.4 æ ¡éªå½åç¶ææ¯å¦å
许ä½åº |
| | | boolean wasProcessing = CrmSaleQuotationStatusEnum.PROCESS.getStatus().equals(saleQuotation.getStatus()); |
| | | if (!wasProcessing |
| | | && !CrmSaleQuotationStatusEnum.DRAFT.getStatus().equals(saleQuotation.getStatus()) |
| | | && !CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus()) |
| | | && !CrmSaleQuotationStatusEnum.REJECT.getStatus().equals(saleQuotation.getStatus())) { |
| | | throw exception(SALE_QUOTATION_VOID_FAIL_STATUS, saleQuotation.getNo(), |
| | | getStatusName(saleQuotation.getStatus())); |
| | | } |
| | | |
| | | // 2.1 å
è½åºä¸ºå·²ä½åºï¼BPM æ¤éåè°åªå¤ç审æ¹ä¸ç¶æï¼ä¸ä¼è¦çæ¬ç¶æ |
| | | saleQuotationMapper.updateById(new CrmSaleQuotationDO() |
| | | .setId(id) |
| | | .setStatus(CrmSaleQuotationStatusEnum.VOID.getStatus()) |
| | | .setVoidType(CrmSaleQuotationVoidTypeEnum.MANUAL.getType()) |
| | | .setVoidReason(trimmedReason) |
| | | .setVoidTime(LocalDateTime.now()) |
| | | .setVoidBy(getLoginUserId())); |
| | | // 2.2 审æ¹ä¸çæ¥ä»·å忥æ¤éæµç¨ï¼é¿å
审æ¹äººç»§ç»å¤çå·²ä½åºçæ¥ä»· |
| | | if (wasProcessing) { |
| | | cancelProcessQuietly(saleQuotation.getProcessInstanceId(), trimmedReason); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public List<CrmSaleQuotationRevisionDO> getSaleQuotationRevisionList(Long quotationId) { |
| | | validateSaleQuotationExists(quotationId); |
| | | return revisionMapper.selectListByQuotationId(quotationId); |
| | | } |
| | | |
| | | @Override |
| | | public List<CrmSaleQuotationDO> getBindableQuotationList(Long customerId, Long bidBondId) { |
| | | if (customerId == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<CrmSaleQuotationDO> quotations = saleQuotationMapper.selectListByCustomerIdAndStatuses(customerId, |
| | | List.of(CrmSaleQuotationStatusEnum.DRAFT.getStatus(), |
| | | CrmSaleQuotationStatusEnum.APPROVE.getStatus(), |
| | | CrmSaleQuotationStatusEnum.REJECT.getStatus())); |
| | | // è¿ææ¥ä»·åä¼å¨æä½åºï¼éå
ä»åéä¸åé¤ |
| | | expiryHelper.expireIfNeeded(quotations); |
| | | return quotations.stream() |
| | | .filter(q -> q.getBidBondId() == null || Objects.equals(q.getBidBondId(), bidBondId)) |
| | | .collect(java.util.stream.Collectors.toList()); |
| | | } |
| | | |
| | | /** |
| | | * è®°å½æ¬è½®æ¥ä»·æµæ°´ï¼ä¾äºæ¬¡/䏿¬¡æ¥ä»·è¿½æº¯ã |
| | | */ |
| | | private void recordRevision(CrmSaleQuotationDO saleQuotation, Long userId) { |
| | | int revisionNo = (saleQuotation.getRevisionNo() == null ? 0 : saleQuotation.getRevisionNo()) + 1; |
| | | if (revisionMapper.selectByQuotationIdAndRevisionNo(saleQuotation.getId(), revisionNo) != null) { |
| | | throw exception(SALE_QUOTATION_REVISION_EXISTS, saleQuotation.getNo(), revisionNo); |
| | | } |
| | | List<CrmSaleQuotationProductDO> products = |
| | | saleQuotationProductMapper.selectListByQuotationId(saleQuotation.getId()); |
| | | revisionMapper.insert(CrmSaleQuotationRevisionDO.builder() |
| | | .quotationId(saleQuotation.getId()) |
| | | .quotationNo(saleQuotation.getNo()) |
| | | .revisionNo(revisionNo) |
| | | .bidBondId(saleQuotation.getBidBondId()) |
| | | .bidBondNo(saleQuotation.getBidBondNo()) |
| | | .customerId(saleQuotation.getCustomerId()) |
| | | .name(saleQuotation.getName()) |
| | | .quotationTime(saleQuotation.getQuotationTime()) |
| | | .validUntil(saleQuotation.getValidUntil()) |
| | | .totalProductPrice(saleQuotation.getTotalProductPrice()) |
| | | .discountPercent(saleQuotation.getDiscountPercent()) |
| | | .discountPrice(saleQuotation.getDiscountPrice()) |
| | | .totalPrice(saleQuotation.getTotalPrice()) |
| | | .taxRate(saleQuotation.getTaxRate()) |
| | | .taxPrice(saleQuotation.getTaxPrice()) |
| | | .itemsSnapshot(buildItemsSnapshot(products)) |
| | | .status(CrmSaleQuotationStatusEnum.PROCESS.getStatus()) |
| | | .submitUserId(userId) |
| | | .submitTime(LocalDateTime.now()) |
| | | .build()); |
| | | // 忥æ¥ä»·å轮次 |
| | | saleQuotationMapper.updateById(new CrmSaleQuotationDO() |
| | | .setId(saleQuotation.getId()) |
| | | .setRevisionNo(revisionNo)); |
| | | saleQuotation.setRevisionNo(revisionNo); |
| | | // åæ¥ææ è®°å½ä¸çå½åæ¥ä»·è½®æ¬¡ |
| | | if (saleQuotation.getBidBondId() != null) { |
| | | bidBondMapper.updateById(new CrmBidBondDO() |
| | | .setId(saleQuotation.getBidBondId()) |
| | | .setQuotationRound(revisionNo)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ¯å¦å
è®¸éæ°æ¥ä»·ï¼ç»å®ææ ãæ¥ä»·å·²å®¡æ¹è¿ã䏿æ ç»æä¸ºãæªä¸æ ãã |
| | | */ |
| | | private boolean canRequote(CrmSaleQuotationDO saleQuotation) { |
| | | if (saleQuotation.getBidBondId() == null) { |
| | | return false; |
| | | } |
| | | if (!CrmSaleQuotationStatusEnum.APPROVE.getStatus().equals(saleQuotation.getStatus()) |
| | | && !CrmSaleQuotationStatusEnum.REJECT.getStatus().equals(saleQuotation.getStatus())) { |
| | | return false; |
| | | } |
| | | CrmBidBondDO bidBond = bidBondMapper.selectById(saleQuotation.getBidBondId()); |
| | | return bidBond != null && CrmBidResultEnum.LOST.getResult().equals(bidBond.getBidResult()); |
| | | } |
| | | |
| | | /** |
| | | * å·²ä½åºçæ¥ä»·åä¸å
è®¸ç»§ç»æä½ï¼æä½åºç±»åç»åºç²¾ç¡®æç¤ºã |
| | | */ |
| | | private void validateNotVoid(CrmSaleQuotationDO saleQuotation) { |
| | | if (!CrmSaleQuotationStatusEnum.VOID.getStatus().equals(saleQuotation.getStatus())) { |
| | | return; |
| | | } |
| | | if (CrmSaleQuotationVoidTypeEnum.EXPIRED.getType().equals(saleQuotation.getVoidType())) { |
| | | String validUntil = saleQuotation.getValidUntil() != null |
| | | ? saleQuotation.getValidUntil().format(DATE_FORMATTER) : "æªç¥"; |
| | | throw exception(SALE_QUOTATION_EXPIRED, saleQuotation.getNo(), validUntil); |
| | | } |
| | | throw exception(SALE_QUOTATION_VOIDED, saleQuotation.getNo(), |
| | | StrUtil.blankToDefault(saleQuotation.getVoidReason(), "æªå¡«åä½åºåå ")); |
| | | } |
| | | |
| | | private String getStatusName(Integer status) { |
| | | CrmSaleQuotationStatusEnum statusEnum = CrmSaleQuotationStatusEnum.getByStatus(status); |
| | | return statusEnum != null ? statusEnum.getName() : "æªç¥ç¶æ(" + status + ")"; |
| | | } |
| | | |
| | | private String getBidResultName(Integer result) { |
| | | CrmBidResultEnum resultEnum = CrmBidResultEnum.valueOf(result); |
| | | return resultEnum != null ? resultEnum.getName() : "æªç¥ç»æ(" + result + ")"; |
| | | } |
| | | |
| | | private String buildItemsSnapshot(List<CrmSaleQuotationProductDO> products) { |
| | | if (CollUtil.isEmpty(products)) { |
| | | return JsonUtils.toJsonString(Collections.emptyList()); |
| | | } |
| | | Set<Long> itemIds = convertSet(products, CrmSaleQuotationProductDO::getItemId); |
| | | Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(itemIds); |
| | | List<Map<String, Object>> items = convertList(products, product -> { |
| | | MdmItemRespDTO item = itemMap.get(product.getItemId()); |
| | | Map<String, Object> snapshot = new LinkedHashMap<>(); |
| | | snapshot.put("itemId", product.getItemId()); |
| | | snapshot.put("itemCode", item != null ? item.getCode() : null); |
| | | snapshot.put("itemName", item != null ? item.getName() : null); |
| | | snapshot.put("itemSpecification", item != null ? item.getSpecification() : null); |
| | | snapshot.put("count", product.getCount()); |
| | | snapshot.put("itemPrice", product.getItemPrice()); |
| | | snapshot.put("quotationPrice", product.getQuotationPrice()); |
| | | snapshot.put("taxPercent", product.getTaxPercent()); |
| | | snapshot.put("totalPrice", product.getTotalPrice()); |
| | | snapshot.put("taxPrice", product.getTaxPrice()); |
| | | snapshot.put("remark", product.getRemark()); |
| | | return snapshot; |
| | | }); |
| | | return JsonUtils.toJsonString(items); |
| | | } |
| | | |
| | | private void cancelProcessQuietly(String processInstanceId, String reason) { |
| | | if (StrUtil.isBlank(processInstanceId)) { |
| | | return; |
| | | } |
| | | try { |
| | | bpmProcessInstanceApi.cancelProcessInstanceByAdmin(getLoginUserId(), processInstanceId, reason); |
| | | } catch (Exception ex) { |
| | | log.warn("[cancelProcessQuietly][æ¥ä»·åæµç¨({})æ¤é失败ï¼ä¸å½±åä½åº]", processInstanceId, ex); |
| | | } |
| | | } |
| | | |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.service.bidbond; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.crm.controller.admin.bidbond.vo.CrmBidBondSaveReqVO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.lang.reflect.Proxy; |
| | | import java.math.BigDecimal; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BID_BOND_QUOTATION_ALREADY_BOUND; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BID_BOND_QUOTATION_CUSTOMER_MISMATCH; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BID_BOND_QUOTATION_STATUS_INVALID; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.BID_BOND_WON_QUOTATION_REQUIRED; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | class CrmBidBondQuotationBindingTest { |
| | | |
| | | private final CrmBidBondServiceImpl service = new CrmBidBondServiceImpl(); |
| | | private final AtomicReference<CrmBidBondDO> inserted = new AtomicReference<>(); |
| | | private final AtomicReference<Object[]> bound = new AtomicReference<>(); |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | CrmCustomerService customerService = stub(CrmCustomerService.class, (proxy, method, args) -> { |
| | | if ("getCustomer".equals(method.getName())) { |
| | | return new CrmCustomerDO().setId((Long) args[0]).setName("客æ·1"); |
| | | } |
| | | throw new AssertionError(method.getName()); |
| | | }); |
| | | CrmBidBondMapper bondMapper = stub(CrmBidBondMapper.class, (proxy, method, args) -> switch (method.getName()) { |
| | | case "selectByNo" -> null; |
| | | case "selectById" -> new CrmBidBondDO().setId((Long) args[0]).setNo("BZ-EXIST"); |
| | | case "insert" -> { |
| | | CrmBidBondDO bond = (CrmBidBondDO) args[0]; |
| | | bond.setId(100L); |
| | | inserted.set(bond); |
| | | yield 1; |
| | | } |
| | | default -> throw new AssertionError(method.getName()); |
| | | }); |
| | | ReflectionTestUtils.setField(service, "customerService", customerService); |
| | | ReflectionTestUtils.setField(service, "bidBondMapper", bondMapper); |
| | | } |
| | | |
| | | private void bindQuotation(CrmSaleQuotationDO quotation) { |
| | | CrmSaleQuotationMapper quoteMapper = stub(CrmSaleQuotationMapper.class, |
| | | (proxy, method, args) -> switch (method.getName()) { |
| | | case "selectById" -> quotation; |
| | | case "bindBidBond" -> { |
| | | bound.set(args); |
| | | yield null; |
| | | } |
| | | default -> throw new AssertionError(method.getName()); |
| | | }); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper); |
| | | } |
| | | |
| | | @Test |
| | | void customerMismatchRejected() { |
| | | bindQuotation(new CrmSaleQuotationDO().setId(5L).setNo("BJ5").setCustomerId(2L).setStatus(2)); |
| | | CrmBidBondSaveReqVO req = baseReq(); |
| | | req.setQuotationId(5L); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.createBidBond(req)); |
| | | assertEquals(BID_BOND_QUOTATION_CUSTOMER_MISMATCH.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void voidQuotationCannotBeBound() { |
| | | bindQuotation(new CrmSaleQuotationDO().setId(5L).setNo("BJ5").setCustomerId(1L).setStatus(5)); |
| | | CrmBidBondSaveReqVO req = baseReq(); |
| | | req.setQuotationId(5L); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.createBidBond(req)); |
| | | assertEquals(BID_BOND_QUOTATION_STATUS_INVALID.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void quotationAlreadyBoundRejected() { |
| | | bindQuotation(new CrmSaleQuotationDO().setId(5L).setNo("BJ5").setCustomerId(1L).setStatus(2) |
| | | .setBidBondId(8L)); |
| | | CrmBidBondSaveReqVO req = baseReq(); |
| | | req.setQuotationId(5L); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.createBidBond(req)); |
| | | assertEquals(BID_BOND_QUOTATION_ALREADY_BOUND.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void wonRequiresQuotation() { |
| | | bindQuotation(new CrmSaleQuotationDO().setId(5L).setNo("BJ5").setCustomerId(1L).setStatus(2)); |
| | | CrmBidBondSaveReqVO req = baseReq(); |
| | | req.setBidResult(1); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.createBidBond(req)); |
| | | assertEquals(BID_BOND_WON_QUOTATION_REQUIRED.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void createBindsQuotationBack() { |
| | | bindQuotation(new CrmSaleQuotationDO().setId(5L).setNo("BJ5").setCustomerId(1L).setStatus(2) |
| | | .setRevisionNo(2)); |
| | | CrmBidBondSaveReqVO req = baseReq(); |
| | | req.setQuotationId(5L); |
| | | |
| | | service.createBidBond(req); |
| | | |
| | | assertNotNull(inserted.get()); |
| | | assertEquals("BJ5", inserted.get().getQuotationNo()); |
| | | assertEquals(2, inserted.get().getQuotationRound()); |
| | | assertNotNull(bound.get()); |
| | | assertEquals(5L, bound.get()[0]); |
| | | assertEquals(100L, bound.get()[1]); |
| | | assertEquals(inserted.get().getNo(), bound.get()[2]); |
| | | } |
| | | |
| | | private CrmBidBondSaveReqVO baseReq() { |
| | | CrmBidBondSaveReqVO req = new CrmBidBondSaveReqVO(); |
| | | req.setCustomerId(1L); |
| | | req.setProjectName("ææé¡¹ç®"); |
| | | req.setBondAmount(new BigDecimal("1000")); |
| | | req.setBidResult(0); |
| | | return req; |
| | | } |
| | | |
| | | private static <T> T stub(Class<T> type, java.lang.reflect.InvocationHandler handler) { |
| | | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.service.quotation; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.lang.reflect.Proxy; |
| | | |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_CONVERT_FAIL_BID_NOT_WON; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_CONVERT_FAIL_BID_QUOTATION_MISMATCH; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_CONVERT_FAIL_BID_REQUIRED; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | class CrmSaleQuotationConvertGateTest { |
| | | |
| | | private final CrmSaleQuotationServiceImpl service = new CrmSaleQuotationServiceImpl(); |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | ReflectionTestUtils.setField(service, "expiryHelper", new CrmSaleQuotationExpiryHelper()); |
| | | } |
| | | |
| | | @Test |
| | | void needBidQuotationWithoutBondCannotConvert() { |
| | | prepare(new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2).setNeedBid(true), null); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.convertToContract(1L, 291L)); |
| | | assertEquals(SALE_QUOTATION_CONVERT_FAIL_BID_REQUIRED.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void nullNeedBidTreatedAsRequired() { |
| | | prepare(new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2), null); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.convertToContract(1L, 291L)); |
| | | assertEquals(SALE_QUOTATION_CONVERT_FAIL_BID_REQUIRED.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void notNeedBidQuotationUsesOriginalStatusRule() { |
| | | prepare(new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(1).setNeedBid(false), null); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.convertToContract(1L, 291L)); |
| | | assertEquals(SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void boundQuotationRequiresWonBid() { |
| | | prepare(new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2).setBidBondId(9L), |
| | | new CrmBidBondDO().setId(9L).setNo("BZ9").setBidResult(2).setQuotationId(1L)); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.convertToContract(1L, 291L)); |
| | | assertEquals(SALE_QUOTATION_CONVERT_FAIL_BID_NOT_WON.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void boundQuotationRequiresMatchingBid() { |
| | | prepare(new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2).setBidBondId(9L), |
| | | new CrmBidBondDO().setId(9L).setNo("BZ9").setBidResult(1).setQuotationId(8L)); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.convertToContract(1L, 291L)); |
| | | assertEquals(SALE_QUOTATION_CONVERT_FAIL_BID_QUOTATION_MISMATCH.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void wonQuotationMustBeApproved() { |
| | | prepare(new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(0).setBidBondId(9L), |
| | | new CrmBidBondDO().setId(9L).setNo("BZ9").setBidResult(1).setQuotationId(1L)); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, () -> service.convertToContract(1L, 291L)); |
| | | assertEquals(SALE_QUOTATION_CONVERT_FAIL_NOT_DRAFT.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | private void prepare(CrmSaleQuotationDO quote, CrmBidBondDO bond) { |
| | | CrmSaleQuotationMapper quoteMapper = stub(CrmSaleQuotationMapper.class, |
| | | (proxy, method, args) -> { |
| | | if ("selectById".equals(method.getName())) { |
| | | return quote; |
| | | } |
| | | throw new AssertionError(method.getName()); |
| | | }); |
| | | CrmBidBondMapper bondMapper = stub(CrmBidBondMapper.class, |
| | | (proxy, method, args) -> { |
| | | if ("selectById".equals(method.getName())) { |
| | | return bond; |
| | | } |
| | | throw new AssertionError(method.getName()); |
| | | }); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper); |
| | | ReflectionTestUtils.setField(service, "bidBondMapper", bondMapper); |
| | | } |
| | | |
| | | private static <T> T stub(Class<T> type, java.lang.reflect.InvocationHandler handler) { |
| | | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.service.quotation; |
| | | |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.lang.reflect.Proxy; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | |
| | | class CrmSaleQuotationExpiryHelperTest { |
| | | |
| | | private final CrmSaleQuotationExpiryHelper helper = new CrmSaleQuotationExpiryHelper(); |
| | | |
| | | @Test |
| | | void isExpired_nullNeverExpires() { |
| | | assertFalse(helper.isExpired(null)); |
| | | } |
| | | |
| | | @Test |
| | | void isExpired_dateOnlyRemainsValidToday() { |
| | | // åç«¯æ¥ææ§ä»¶åªéå°æ¥ï¼æ¶é´é¨å为 00:00:00ï¼å½å¤©åºä»ææ |
| | | assertFalse(helper.isExpired(LocalDate.now().atStartOfDay())); |
| | | assertTrue(helper.isExpired(LocalDate.now().minusDays(1).atStartOfDay())); |
| | | } |
| | | |
| | | @Test |
| | | void isExpired_exactTimeCompare() { |
| | | assertFalse(helper.isExpired(LocalDateTime.now().plusMinutes(1))); |
| | | assertTrue(helper.isExpired(LocalDateTime.now().minusMinutes(1))); |
| | | } |
| | | |
| | | @Test |
| | | void expiredQuotationIsVoided() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(0) |
| | | .setValidUntil(LocalDate.now().minusDays(2).atStartOfDay()); |
| | | AtomicReference<CrmSaleQuotationDO> updated = new AtomicReference<>(); |
| | | CrmSaleQuotationMapper quoteMapper = stub(CrmSaleQuotationMapper.class, |
| | | (proxy, method, args) -> switch (method.getName()) { |
| | | case "updateById" -> { |
| | | updated.set((CrmSaleQuotationDO) args[0]); |
| | | yield 1; |
| | | } |
| | | default -> throw new AssertionError(method.getName()); |
| | | }); |
| | | ReflectionTestUtils.setField(helper, "saleQuotationMapper", quoteMapper); |
| | | ReflectionTestUtils.setField(helper, "bidBondMapper", emptyBondMapper()); |
| | | |
| | | helper.expireIfNeeded(quote); |
| | | |
| | | assertEquals(5, quote.getStatus()); |
| | | assertEquals(2, quote.getVoidType()); |
| | | assertNotNull(quote.getVoidReason()); |
| | | assertNotNull(updated.get()); |
| | | assertEquals(5, updated.get().getStatus()); |
| | | assertEquals(2, updated.get().getVoidType()); |
| | | } |
| | | |
| | | @Test |
| | | void bidInProgressIsSkipped() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2).setBidBondId(9L) |
| | | .setValidUntil(LocalDate.now().minusDays(2).atStartOfDay()); |
| | | CrmSaleQuotationMapper quoteMapper = stub(CrmSaleQuotationMapper.class, |
| | | (proxy, method, args) -> { |
| | | throw new AssertionError("ææ è¿è¡ä¸ä¸åºèªå¨ä½åºï¼" + method.getName()); |
| | | }); |
| | | CrmBidBondMapper bondMapper = stub(CrmBidBondMapper.class, |
| | | (proxy, method, args) -> List.of(new CrmBidBondDO().setId(9L).setBidResult(0))); |
| | | ReflectionTestUtils.setField(helper, "saleQuotationMapper", quoteMapper); |
| | | ReflectionTestUtils.setField(helper, "bidBondMapper", bondMapper); |
| | | |
| | | helper.expireIfNeeded(quote); |
| | | |
| | | assertEquals(2, quote.getStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void lostBidQuotationIsExpired() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2).setBidBondId(9L) |
| | | .setValidUntil(LocalDate.now().minusDays(2).atStartOfDay()); |
| | | AtomicReference<CrmSaleQuotationDO> updated = new AtomicReference<>(); |
| | | CrmSaleQuotationMapper quoteMapper = stub(CrmSaleQuotationMapper.class, |
| | | (proxy, method, args) -> switch (method.getName()) { |
| | | case "updateById" -> { |
| | | updated.set((CrmSaleQuotationDO) args[0]); |
| | | yield 1; |
| | | } |
| | | default -> throw new AssertionError(method.getName()); |
| | | }); |
| | | CrmBidBondMapper bondMapper = stub(CrmBidBondMapper.class, |
| | | (proxy, method, args) -> List.of(new CrmBidBondDO().setId(9L).setBidResult(2))); |
| | | ReflectionTestUtils.setField(helper, "saleQuotationMapper", quoteMapper); |
| | | ReflectionTestUtils.setField(helper, "bidBondMapper", bondMapper); |
| | | |
| | | helper.expireIfNeeded(quote); |
| | | |
| | | assertEquals(5, quote.getStatus()); |
| | | assertEquals(2, quote.getVoidType()); |
| | | } |
| | | |
| | | private static CrmBidBondMapper emptyBondMapper() { |
| | | return stub(CrmBidBondMapper.class, (proxy, method, args) -> List.of()); |
| | | } |
| | | |
| | | private static <T> T stub(Class<T> type, java.lang.reflect.InvocationHandler handler) { |
| | | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler)); |
| | | } |
| | | } |
| | |
| | | void conversionKeepsFractionalQuantity() { |
| | | var service = new CrmSaleQuotationServiceImpl(); |
| | | var request = new AtomicReference<CrmContractSaveReqVO>(); |
| | | var quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ-1").setName("å°è´¦æ¥ä»·").setStatus(0); |
| | | var quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ-1").setName("å°è´¦æ¥ä»·").setStatus(0) |
| | | .setNeedBid(false); |
| | | var quotes = stub(CrmSaleQuotationMapper.class, (proxy, method, args) -> switch (method.getName()) { |
| | | case "selectById" -> quote; |
| | | case "updateById" -> 1; |
| | |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quotes); |
| | | ReflectionTestUtils.setField(service, "saleQuotationProductMapper", products); |
| | | ReflectionTestUtils.setField(service, "contractService", contracts); |
| | | ReflectionTestUtils.setField(service, "expiryHelper", new CrmSaleQuotationExpiryHelper()); |
| | | assertEquals(2L, service.convertToContract(1L, 291L)); |
| | | assertEquals(new BigDecimal("22304.8"), request.get().getProducts().getFirst().getCount()); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.service.quotation; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | 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.quotation.CrmSaleQuotationRevisionDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationProductMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationRevisionMapper; |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.lang.reflect.Proxy; |
| | | import java.math.BigDecimal; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_REVISION_EXISTS; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | class CrmSaleQuotationRevisionTest { |
| | | |
| | | private final CrmSaleQuotationServiceImpl service = new CrmSaleQuotationServiceImpl(); |
| | | private final List<CrmSaleQuotationRevisionDO> inserted = new ArrayList<>(); |
| | | |
| | | private void prepare(long existingRevisionCount) { |
| | | CrmSaleQuotationRevisionMapper revisionMapper = stub(CrmSaleQuotationRevisionMapper.class, |
| | | (proxy, method, args) -> switch (method.getName()) { |
| | | case "selectByQuotationIdAndRevisionNo" -> |
| | | (Integer) args[1] <= existingRevisionCount ? new CrmSaleQuotationRevisionDO() : null; |
| | | case "insert" -> { |
| | | inserted.add((CrmSaleQuotationRevisionDO) args[0]); |
| | | yield 1; |
| | | } |
| | | default -> throw new AssertionError(method.getName()); |
| | | }); |
| | | CrmSaleQuotationProductMapper productMapper = stub(CrmSaleQuotationProductMapper.class, |
| | | (proxy, method, args) -> { |
| | | if ("selectListByQuotationId".equals(method.getName())) { |
| | | return List.of(new CrmSaleQuotationProductDO().setItemId(1103L) |
| | | .setCount(new BigDecimal("2")) |
| | | .setQuotationPrice(new BigDecimal("100"))); |
| | | } |
| | | throw new AssertionError(method.getName()); |
| | | }); |
| | | CrmSaleQuotationMapper quoteMapper = stub(CrmSaleQuotationMapper.class, |
| | | (proxy, method, args) -> 1); |
| | | MdmItemApi itemApi = stub(MdmItemApi.class, (proxy, method, args) -> Map.of()); |
| | | ReflectionTestUtils.setField(service, "revisionMapper", revisionMapper); |
| | | ReflectionTestUtils.setField(service, "saleQuotationProductMapper", productMapper); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper); |
| | | ReflectionTestUtils.setField(service, "mdmItemApi", itemApi); |
| | | } |
| | | |
| | | @Test |
| | | void eachSubmitIncrementsRevision() { |
| | | prepare(0); |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setRevisionNo(0); |
| | | |
| | | ReflectionTestUtils.invokeMethod(service, "recordRevision", quote, 291L); |
| | | ReflectionTestUtils.invokeMethod(service, "recordRevision", quote, 291L); |
| | | |
| | | assertEquals(2, inserted.size()); |
| | | assertEquals(1, inserted.get(0).getRevisionNo()); |
| | | assertEquals(2, inserted.get(1).getRevisionNo()); |
| | | assertEquals(2, quote.getRevisionNo()); |
| | | assertNotNull(inserted.get(0).getItemsSnapshot()); |
| | | assertEquals(1, inserted.get(0).getStatus()); |
| | | } |
| | | |
| | | @Test |
| | | void duplicateRevisionRejected() { |
| | | prepare(1); |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setRevisionNo(0); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, |
| | | () -> ReflectionTestUtils.invokeMethod(service, "recordRevision", quote, 291L)); |
| | | assertEquals(SALE_QUOTATION_REVISION_EXISTS.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | private static <T> T stub(Class<T> type, java.lang.reflect.InvocationHandler handler) { |
| | | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler)); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.crm.service.quotation; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.bidbond.CrmBidBondDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.quotation.CrmSaleQuotationDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.bidbond.CrmBidBondMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.quotation.CrmSaleQuotationMapper; |
| | | import org.junit.jupiter.api.BeforeEach; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.lang.reflect.Proxy; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_VOID_FAIL_STATUS; |
| | | import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.SALE_QUOTATION_VOID_FAIL_WON; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | |
| | | class CrmSaleQuotationVoidTest { |
| | | |
| | | private final CrmSaleQuotationServiceImpl service = new CrmSaleQuotationServiceImpl(); |
| | | |
| | | @BeforeEach |
| | | void setUp() { |
| | | ReflectionTestUtils.setField(service, "expiryHelper", new CrmSaleQuotationExpiryHelper()); |
| | | } |
| | | |
| | | @Test |
| | | void voidDraftQuotation() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(0); |
| | | AtomicReference<CrmSaleQuotationDO> updated = new AtomicReference<>(); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper(quote, updated)); |
| | | |
| | | service.voidSaleQuotation(1L, " 客æ·åæ¶æ¬æ¬¡éè´ "); |
| | | |
| | | assertNotNull(updated.get()); |
| | | assertEquals(5, updated.get().getStatus()); |
| | | assertEquals(1, updated.get().getVoidType()); |
| | | assertEquals("客æ·åæ¶æ¬æ¬¡éè´", updated.get().getVoidReason()); |
| | | assertNotNull(updated.get().getVoidTime()); |
| | | } |
| | | |
| | | @Test |
| | | void voidWonQuotationRejected() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(2).setBidBondId(9L); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper(quote, new AtomicReference<>())); |
| | | ReflectionTestUtils.setField(service, "bidBondMapper", bondMapper(new CrmBidBondDO().setId(9L).setBidResult(1))); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, |
| | | () -> service.voidSaleQuotation(1L, "ä¸ååä¸")); |
| | | assertEquals(SALE_QUOTATION_VOID_FAIL_WON.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void voidConvertedQuotationRejected() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(10); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper(quote, new AtomicReference<>())); |
| | | |
| | | ServiceException ex = assertThrows(ServiceException.class, |
| | | () -> service.voidSaleQuotation(1L, "ä¸ååä¸")); |
| | | assertEquals(SALE_QUOTATION_VOID_FAIL_STATUS.getCode(), ex.getCode()); |
| | | } |
| | | |
| | | @Test |
| | | void voidProcessingQuotationCancelsProcess() { |
| | | CrmSaleQuotationDO quote = new CrmSaleQuotationDO().setId(1L).setNo("BJ1").setStatus(1) |
| | | .setProcessInstanceId("proc-1"); |
| | | AtomicReference<CrmSaleQuotationDO> updated = new AtomicReference<>(); |
| | | ReflectionTestUtils.setField(service, "saleQuotationMapper", quoteMapper(quote, updated)); |
| | | AtomicReference<String[]> cancelled = new AtomicReference<>(); |
| | | ReflectionTestUtils.setField(service, "bpmProcessInstanceApi", stub(BpmProcessInstanceApi.class, |
| | | (proxy, method, args) -> { |
| | | if ("cancelProcessInstanceByAdmin".equals(method.getName())) { |
| | | cancelled.set(new String[]{(String) args[1], (String) args[2]}); |
| | | return null; |
| | | } |
| | | throw new AssertionError(method.getName()); |
| | | })); |
| | | |
| | | service.voidSaleQuotation(1L, "项ç®åæ¶"); |
| | | |
| | | assertEquals(5, updated.get().getStatus()); |
| | | assertNotNull(cancelled.get()); |
| | | assertEquals("proc-1", cancelled.get()[0]); |
| | | assertEquals("项ç®åæ¶", cancelled.get()[1]); |
| | | } |
| | | |
| | | private CrmSaleQuotationMapper quoteMapper(CrmSaleQuotationDO quote, AtomicReference<CrmSaleQuotationDO> updated) { |
| | | return stub(CrmSaleQuotationMapper.class, (proxy, method, args) -> switch (method.getName()) { |
| | | case "selectById" -> quote; |
| | | case "updateById" -> { |
| | | updated.set((CrmSaleQuotationDO) args[0]); |
| | | yield 1; |
| | | } |
| | | default -> throw new AssertionError(method.getName()); |
| | | }); |
| | | } |
| | | |
| | | private CrmBidBondMapper bondMapper(CrmBidBondDO bond) { |
| | | return stub(CrmBidBondMapper.class, (proxy, method, args) -> { |
| | | if ("selectById".equals(method.getName())) { |
| | | return bond; |
| | | } |
| | | throw new AssertionError(method.getName()); |
| | | }); |
| | | } |
| | | |
| | | private static <T> T stub(Class<T> type, java.lang.reflect.InvocationHandler handler) { |
| | | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler)); |
| | | } |
| | | } |