diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/controller/CrmQuoteInfoController.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/controller/CrmQuoteInfoController.java index 2a81e939..44948354 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/controller/CrmQuoteInfoController.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/controller/CrmQuoteInfoController.java @@ -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 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 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 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()); + } + } + } +