feat(crm): 导出报价单模板功能

- 新增导出报价单模板接口,支持根据报价单ID导出Excel文件
- 使用模板文件填充主表信息和明细数据,实现主表和明细混合填充
- 处理缺失报价单的异常情况,抛出业务异常
- 明细无数据时添加空行,避免导出异常
- 使用金额数字转中文大写格式,格式化金额字段
- 添加日志记录导出异常和警告信息
- 依赖Hutool和ExcelUtil工具类进行日期格式化和Excel操作
dev
zangch@mesnac.com 3 weeks ago
parent fca9418657
commit 11e7303f8e

@ -1,11 +1,17 @@
package org.dromara.oa.crm.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.NumberChineseFormatter;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
@ -16,12 +22,15 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.oa.crm.domain.bo.CrmQuoteInfoBo;
import org.dromara.oa.crm.domain.dto.QuoteTemplateMaterialDto;
import org.dromara.oa.crm.domain.vo.CrmQuoteInfoVo;
import org.dromara.oa.crm.domain.vo.CrmQuoteMaterialVo;
import org.dromara.oa.crm.service.ICrmQuoteInfoService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.math.BigDecimal;
import java.util.*;
/**
*
@ -30,6 +39,7 @@ import java.util.List;
* @author Yinq
* @date 2025-10-28
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@ -130,4 +140,89 @@ public class CrmQuoteInfoController extends BaseController {
return R.ok(crmQuoteInfoService.recalcTotals(quoteId));
}
/**
*
*
* @param quoteId ID
*/
@SaCheckPermission("oa/crm:crmQuoteInfo:export")
@Log(title = "导出报价单模板", businessType = BusinessType.EXPORT)
@GetMapping("/exportTemplate/{quoteId}")
public void exportQuoteTemplate(@PathVariable("quoteId") Long quoteId, HttpServletResponse response) {
// 获取报价单详细信息
CrmQuoteInfoVo quoteInfo = crmQuoteInfoService.queryById(quoteId);
if (quoteInfo == null) {
throw new ServiceException("报价单不存在");
}
// 准备Map形式的主表数据对应模板中的{key}占位符,注意不是{map.key}
Map<String, String> templateData = new HashMap<>();
templateData.put("customerContactName", quoteInfo.getCustomerContactName() != null ? quoteInfo.getCustomerContactName() : "");
templateData.put("title", quoteInfo.getQuoteName() != null ? quoteInfo.getQuoteName() : "");
templateData.put("quoteDate", quoteInfo.getQuoteDate() != null ? DateUtil.format(quoteInfo.getQuoteDate(), "yyyy年MM月dd日") : "");
templateData.put("taxIncludedInfo", quoteInfo.getTaxIncludedInfo() != null ? quoteInfo.getTaxIncludedInfo() : "");
templateData.put("paymentMethod", quoteInfo.getPaymentMethod() != null ? quoteInfo.getPaymentMethod() : "");
templateData.put("deliveryPeriod", quoteInfo.getDeliveryPeriod() != null ? quoteInfo.getDeliveryPeriod() + "天" : "");
templateData.put("deliveryMethod", quoteInfo.getDeliveryMethod() != null ? quoteInfo.getDeliveryMethod() : "");
templateData.put("quoteValidity", quoteInfo.getValidDays() != null ? quoteInfo.getValidDays() + "天" : "");
// 币种
templateData.put("currencyType", quoteInfo.getCurrencyType() != null ? quoteInfo.getCurrencyType() : "人民币");
// 金额相关
BigDecimal totalAmount = quoteInfo.getTotalIncludingTax() != null ? quoteInfo.getTotalIncludingTax() : BigDecimal.ZERO;
templateData.put("total", totalAmount.toString());
templateData.put("totalLower", totalAmount.toString());
templateData.put("totalUpper", NumberChineseFormatter.format(totalAmount.doubleValue(), true, true));
// 供货方信息
templateData.put("supplierContactName", quoteInfo.getSupplierContactName() != null ? quoteInfo.getSupplierContactName() : "");
templateData.put("supplierContactPhone", quoteInfo.getSupplierContactPhone() != null ? quoteInfo.getSupplierContactPhone() : "");
templateData.put("supplierContactEmail", quoteInfo.getSupplierContactEmail() != null ? quoteInfo.getSupplierContactEmail() : "");
// 准备明细数据列表(对应模板中的{.key}占位符)
List<QuoteTemplateMaterialDto> materialList = new ArrayList<>();
if (CollUtil.isNotEmpty(quoteInfo.getItemsVo())) {
for (CrmQuoteMaterialVo material : quoteInfo.getItemsVo()) {
QuoteTemplateMaterialDto materialDto = new QuoteTemplateMaterialDto();
materialDto.setSeq(material.getItemNo() != null ? material.getItemNo().toString() : "");
materialDto.setProductName(material.getProductName() != null ? material.getProductName() : "");
materialDto.setModelDesc(material.getSpecificationDescription() != null ? material.getSpecificationDescription() : "");
materialDto.setQuantity(material.getAmount() != null ? material.getAmount().toString() : "0");
materialDto.setUnit(material.getUnitName() != null ? material.getUnitName() : "");
// 使用含税单价作为单价
materialDto.setPrice(material.getIncludingPrice() != null ? material.getIncludingPrice().toString() : "0");
materialDto.setSubtotal(material.getSubtotal() != null ? material.getSubtotal().toString() : "0");
materialDto.setRemark(material.getRemark() != null ? material.getRemark() : "");
materialList.add(materialDto);
}
}
// 如果没有明细,添加一个空行,避免导出异常
if (materialList.isEmpty()) {
log.warn("报价单 {} 没有明细数据,添加空行", quoteId);
materialList.add(new QuoteTemplateMaterialDto());
}
// 方案A: 主表数据直接放入,占位符用{key}
Map<String, Object> exportData = new LinkedHashMap<>();
// 将主表数据作为一个整体Map放入这样ExcelUtil遍历时会调用fill(map),从而匹配模板中的{key}
exportData.put("mainInfo", templateData);
exportData.put("data", materialList); // 明细数据使用"data"作为key
// 导出Excel模板
String templatePath = "报价单模板.xlsx";
String fileName = "报价单_" + quoteInfo.getQuoteCode() + ".xlsx";
// 打印实际加载的资源路径
ClassPathResource resource = new ClassPathResource(templatePath);
try {
// 使用 ExcelUtil 封装的混合模式导出:主表(Map)原位填充,占位符 {key};明细(List)按 {data.key} 展开
ExcelUtil.exportTemplateMixed(exportData, fileName, templatePath, response);
} catch (Exception e) {
throw new ServiceException("导出报价单失败: " + e.getMessage());
}
}
}

Loading…
Cancel
Save