From 5678e929fce2354cb40840c19164ab2fbf5ee8ee Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Fri, 20 Mar 2026 11:01:15 +0800 Subject: [PATCH] =?UTF-8?q?change(CrmQuoteMaterialVo):=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=8A=A5=E4=BB=B7=E5=8D=95=E7=89=A9=E6=96=99=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E5=AD=90=E8=A1=A8=E7=9A=84=E6=9F=A5=E8=AF=A2=E4=B8=8E?= =?UTF-8?q?=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oa/crm/domain/bo/CrmQuoteMaterialBo.java | 10 ++ .../oa/crm/domain/vo/CrmQuoteMaterialVo.java | 127 ++++++++++++++++-- .../service/impl/CrmQuoteInfoServiceImpl.java | 74 ++++++++++ .../impl/CrmQuoteMaterialServiceImpl.java | 27 +++- 4 files changed, 223 insertions(+), 15 deletions(-) diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/bo/CrmQuoteMaterialBo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/bo/CrmQuoteMaterialBo.java index a9c290c9..5a6c4e1b 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/bo/CrmQuoteMaterialBo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/bo/CrmQuoteMaterialBo.java @@ -32,6 +32,16 @@ public class CrmQuoteMaterialBo extends BaseEntity { */ private Long quoteId; + /** + * 报价单号 + */ + private String quoteCode; + + /** + * 报价单名称 + */ + private String quoteName; + /** * 序号(ERP风格) */ diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/vo/CrmQuoteMaterialVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/vo/CrmQuoteMaterialVo.java index 389fdcf3..3cf3ad83 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/vo/CrmQuoteMaterialVo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/domain/vo/CrmQuoteMaterialVo.java @@ -11,6 +11,7 @@ import org.dromara.oa.crm.domain.CrmQuoteMaterial; import java.io.Serial; import java.io.Serializable; import java.math.BigDecimal; +import java.util.Date; @@ -31,19 +32,118 @@ public class CrmQuoteMaterialVo implements Serializable { /** * 报价物料ID */ - @ExcelProperty(value = "报价物料ID") + // 该字段仅用于系统内部定位明细记录,不属于业务人员导出查看范围,因此不加 Excel 注解。 private Long quoteMaterialId; /** * 报价ID */ - @ExcelProperty(value = "报价ID") + // 该字段是主子表关联键,属于纯技术字段,导出时必须隐藏,避免业务误解为可填写字段。 private Long quoteId; + /** + * 报价单号 + */ + // 导出需要与页面保持一致,先展示报价单号,方便业务人员识别这条明细归属哪一张报价单。 + @ExcelProperty(value = "报价单号") + private String quoteCode; + + /** + * 报价单名称 + */ + // 报价单名称同样属于业务主识别信息,导出时必须保留。 + @ExcelProperty(value = "报价单名称") + private String quoteName; + + /** + * 报价轮次 + */ + // 页面已按你的要求隐藏该字段,导出也保持一致,因此不加 Excel 注解。 + private Long quoteRound; + + /** + * 报价日期 + */ + // 报价日期是业务常用筛选维度,导出保留,便于线下核对版本与时点。 + @ExcelProperty(value = "报价日期") + private Date quoteDate; + + /** + * 报价类型 + */ + // 页面已隐藏“报价类型”,导出也不应出现,避免页面和 Excel 两套口径不一致。 + private String quoteType; + + /** + * 业务方向 + */ + // 页面已隐藏“业务方向”,这里同样只保留对象承载能力,不进入导出列。 + private String businessDirection; + + /** + * 报价单状态 + */ + // 状态是业务关注字段,导出时做字典翻译,避免把 1/2/3 这类编码直接暴露给业务用户。 + @ExcelProperty(value = "报价单状态", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "quote_status") + private String quoteStatus; + + /** + * 付款方式 + */ + // 付款方式对商务沟通很关键,导出时统一转成字典中文,保证线下发给业务也能直接看懂。 + @ExcelProperty(value = "付款方式", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "payment_method") + private String paymentMethod; + + /** + * 币种 + */ + // 币种需要跟金额字段一起导出,否则总价数据脱离币种会失去业务意义。 + @ExcelProperty(value = "币种", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "currency_type") + private String currencyType; + + /** + * 含税信息 + */ + // 含税信息决定金额口径,导出必须保留,避免财务或商务误读总价。 + @ExcelProperty(value = "含税信息") + private String taxIncludedInfo; + + /** + * 总报价 + */ + // 总报价是主表汇总字段,按你的要求同步展示到明细页并导出,方便在明细场景下直接比对整单金额。 + @ExcelProperty(value = "总报价") + private BigDecimal totalPrice; + + /** + * 未税总价 + */ + // 未税总价与税额/含税总价要成组出现,导出时必须保持完整口径。 + @ExcelProperty(value = "未税总价") + private BigDecimal totalBeforeTax; + + /** + * 税额 + */ + // 税额是金额拆分的重要组成项,业务在导出表里经常需要单独核对。 + @ExcelProperty(value = "税额") + private BigDecimal totalTax; + + /** + * 含税总价 + */ + // 含税总价通常是对外报价最直接的金额口径,因此保留为导出字段。 + @ExcelProperty(value = "含税总价") + private BigDecimal totalIncludingTax; + /** * 序号(ERP风格) */ - @ExcelProperty(value = "序号(ERP风格)") + // 页面列名已简化为“序号”,导出也保持一致,避免业务看到系统术语“ERP风格”产生歧义。 + @ExcelProperty(value = "序号") private Long itemNo; /** @@ -54,14 +154,14 @@ public class CrmQuoteMaterialVo implements Serializable { /** * 标准物料标识(1标准物料 2非标物料) */ - @ExcelProperty(value = "标准物料标识", converter = ExcelDictConvert.class) - @ExcelDictFormat(dictType = "material_flag") + // 当前页面未展示该技术/分类字段,导出同样不输出,避免信息噪音过大。 private String materialFlag; /** * 产品/服务名称 */ - @ExcelProperty(value = "产品/服务名称") + // 页面表头已统一用“产品名称”,导出也沿用同口径,避免一个页面两个叫法。 + @ExcelProperty(value = "产品名称") private String productName; /** @@ -73,13 +173,13 @@ public class CrmQuoteMaterialVo implements Serializable { /** * 物料ID(SAP) */ - @ExcelProperty(value = "物料ID(SAP)") + // SAP 物料主键属于技术关联字段,只用于联表,不应导给业务用户。 private Long materialId; /** * 销售物料ID(关联名) */ - @ExcelProperty(value = "销售物料ID(关联名)") + // 销售物料关联主键同样是技术字段,保留在对象中即可,不参与导出。 private Long relationMaterialId; /** @@ -109,13 +209,14 @@ public class CrmQuoteMaterialVo implements Serializable { /** * 单位ID */ - @ExcelProperty(value = "单位ID") + // 单位 ID 仅用于系统内部存储,业务实际只关心单位名称。 private Long unitId; /** * 单位名称 */ - @ExcelProperty(value = "单位名称") + // 页面显示的是“单位”,导出也统一使用该业务口径。 + @ExcelProperty(value = "单位") private String unitName; /** @@ -127,7 +228,8 @@ public class CrmQuoteMaterialVo implements Serializable { /** * 税率 */ - @ExcelProperty(value = "税率") + // 页面列头是“税率(%)”,导出同步保持一致,避免业务以为这里是 0.13 还是 13 两种口径。 + @ExcelProperty(value = "税率(%)") private BigDecimal taxRate; /** @@ -151,8 +253,7 @@ public class CrmQuoteMaterialVo implements Serializable { /** * 激活标识(1是 0否) */ - @ExcelProperty(value = "激活标识", converter = ExcelDictConvert.class) - @ExcelDictFormat(dictType = "active_flag") + // 激活标识不是当前业务查看重点,页面未展示,导出也不输出。 private String activeFlag; diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteInfoServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteInfoServiceImpl.java index 1fa86ee6..5c277aa3 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteInfoServiceImpl.java @@ -20,6 +20,9 @@ import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.oa.base.domain.BaseMaterialInfo; import org.dromara.oa.base.domain.BaseRelationMaterial; +import org.dromara.oa.base.domain.bo.BaseRelationMaterialBo; +import org.dromara.oa.base.domain.vo.BaseRelationMaterialVo; +import org.dromara.oa.base.service.IBaseRelationMaterialService; import org.dromara.oa.crm.domain.*; import org.dromara.oa.crm.domain.bo.CrmQuoteInfoBo; import org.dromara.oa.crm.domain.bo.CrmQuoteMaterialBo; @@ -59,6 +62,7 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService { private final CrmQuoteMaterialMapper quoteMaterialMapper; //客户联系人 private final ICrmCustomerContactService customerContactService; + private final IBaseRelationMaterialService baseRelationMaterialService; @DubboReference(timeout = 30000) private RemoteWorkflowService remoteWorkflowService; @@ -198,6 +202,7 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService { public Boolean insertByBo(CrmQuoteInfoBo bo) { CrmQuoteInfo add = MapstructUtils.convert(bo, CrmQuoteInfo.class); validEntityBeforeSave(add); + Long customerId = resolveCustomerId(bo.getCustomerContactId()); String quoteCode = remoteCodeRuleService.selectCodeRuleCode("1004"); add.setQuoteCode(quoteCode); @@ -218,6 +223,7 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService { if (entity.getItemNo() == null) { entity.setItemNo((long) (i + 1)); } + processRelationMaterial(entity, customerId); quoteMaterialMapper.insert(entity); } // 回写主表金额汇总 @@ -242,6 +248,7 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService { if (!ok) return false; // 差异更新子表:存在则更新,不存在则插入;并删除前端未提交的旧记录 Long qid = bo.getQuoteId(); + Long customerId = resolveCustomerId(bo.getCustomerContactId()); List oldItems = quoteMaterialMapper.selectList( Wrappers.lambdaQuery().eq(CrmQuoteMaterial::getQuoteId, qid) ); @@ -257,6 +264,7 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService { if (entity.getItemNo() == null) { entity.setItemNo((long) (i + 1)); } + processRelationMaterial(entity, customerId); // 使用 insertOrUpdate 简化增改逻辑,参考合同物料实现 quoteMaterialMapper.insertOrUpdate(entity); } @@ -288,6 +296,72 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService { //TODO 做一些数据校验,如唯一约束 } + private Long resolveCustomerId(Long customerContactId) { + // 联系人ID允许为空,因为报价单在草稿或临时保存阶段可能还未选择客户联系人,此时不应抛异常阻断主流程。 + if (customerContactId == null) { + return null; + } + // 报价单主表当前只直接保存客户联系人ID,这里统一通过联系人反查所属客户ID, + // 这样后续物料关联、同轮报价聚合等逻辑都能基于“客户”这一稳定业务主键处理,避免各处重复查询。 + CrmCustomerContactVo customerContactVo = customerContactService.queryById(customerContactId); + // 联系人被删除、数据不完整或跨模块查询未命中时,返回 null 而不是强行报错, + // 目的是让调用方按“未识别到客户”分支继续兜底处理,减少非核心数据缺失对报价保存流程的影响。 + return customerContactVo == null ? null : customerContactVo.getCustomerId(); + } + + private void processRelationMaterial(CrmQuoteMaterial quoteMaterial, Long customerId) { + String materialFlag = StringUtils.isNotBlank(quoteMaterial.getMaterialFlag()) + ? quoteMaterial.getMaterialFlag() + : (quoteMaterial.getMaterialId() != null ? "1" : "2"); + quoteMaterial.setMaterialFlag(materialFlag); + if (!Objects.equals(materialFlag, "1") + || quoteMaterial.getMaterialId() == null + || customerId == null + || StringUtils.isBlank(quoteMaterial.getProductName())) { + return; + } + + // 标准物料允许业务人员修改“产品名称”,因此旧的销售物料关联不一定还能代表当前录入值, + // 这里先校验已带回的 relationMaterialId 是否仍与“物料 + 客户 + 产品名称”一致,不一致就重新匹配/补建。 + BaseRelationMaterialVo currentRelationMaterial = queryRelationMaterial(quoteMaterial.getRelationMaterialId()); + if (currentRelationMaterial != null + && Objects.equals(currentRelationMaterial.getMaterialId(), quoteMaterial.getMaterialId()) + && Objects.equals(currentRelationMaterial.getCustomerId(), customerId) + && StringUtils.equals(currentRelationMaterial.getSaleMaterialName(), quoteMaterial.getProductName())) { + return; + } + quoteMaterial.setRelationMaterialId(null); + + BaseRelationMaterialBo queryBo = new BaseRelationMaterialBo(); + queryBo.setMaterialId(quoteMaterial.getMaterialId()); + queryBo.setCustomerId(customerId); + queryBo.setSaleMaterialName(quoteMaterial.getProductName()); + List relationMaterials = baseRelationMaterialService.queryList(queryBo); + if (relationMaterials != null && !relationMaterials.isEmpty()) { + quoteMaterial.setRelationMaterialId(relationMaterials.get(0).getRelationMaterialId()); + return; + } + + BaseRelationMaterialBo relationMaterialBo = new BaseRelationMaterialBo(); + relationMaterialBo.setMaterialId(quoteMaterial.getMaterialId()); + relationMaterialBo.setCustomerId(customerId); + relationMaterialBo.setSaleMaterialName(quoteMaterial.getProductName()); + relationMaterialBo.setActiveFlag("1"); + // 这里按“物料 + 客户 + 报价产品名称”补建销售物料关联,避免标准物料改名后仍然丢失客户侧展示名称。 + baseRelationMaterialService.insertByBo(relationMaterialBo); + quoteMaterial.setRelationMaterialId(relationMaterialBo.getRelationMaterialId()); + } + + private BaseRelationMaterialVo queryRelationMaterial(Long relationMaterialId) { + if (relationMaterialId == null) { + return null; + } + BaseRelationMaterialBo queryBo = new BaseRelationMaterialBo(); + queryBo.setRelationMaterialId(relationMaterialId); + List relationMaterials = baseRelationMaterialService.queryList(queryBo); + return relationMaterials == null || relationMaterials.isEmpty() ? null : relationMaterials.get(0); + } + /** * 校验并批量删除报价单信息信息 * diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteMaterialServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteMaterialServiceImpl.java index d23110d4..39c5eaa9 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteMaterialServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/crm/service/impl/CrmQuoteMaterialServiceImpl.java @@ -10,6 +10,7 @@ import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.oa.base.domain.BaseMaterialInfo; import org.dromara.oa.base.domain.BaseRelationMaterial; +import org.dromara.oa.crm.domain.CrmQuoteInfo; import org.dromara.oa.crm.domain.CrmQuoteMaterial; import org.dromara.oa.crm.domain.bo.CrmQuoteMaterialBo; import org.dromara.oa.crm.domain.vo.CrmQuoteMaterialVo; @@ -73,14 +74,35 @@ public class CrmQuoteMaterialServiceImpl implements ICrmQuoteMaterialService { MPJLambdaWrapper lqw = JoinWrappers.lambda(CrmQuoteMaterial.class) .selectAll(CrmQuoteMaterial.class) .eq(CrmQuoteMaterial::getDelFlag, "0") - // 联表选择:SAP物料编码/名称、销售物料名称、计量单位名称 + // 这里显式挑选主表业务字段,而不是 selectAll 主表: + // 目的是把“页面展示字段”和“导出字段”控制在同一口径,避免 quoteId、模板ID 这类技术字段混进明细页。 + .selectAs(CrmQuoteInfo::getQuoteCode, CrmQuoteMaterialVo::getQuoteCode) + .selectAs(CrmQuoteInfo::getQuoteName, CrmQuoteMaterialVo::getQuoteName) + .selectAs(CrmQuoteInfo::getQuoteRound, CrmQuoteMaterialVo::getQuoteRound) + .selectAs(CrmQuoteInfo::getQuoteDate, CrmQuoteMaterialVo::getQuoteDate) + .selectAs(CrmQuoteInfo::getQuoteType, CrmQuoteMaterialVo::getQuoteType) + .selectAs(CrmQuoteInfo::getBusinessDirection, CrmQuoteMaterialVo::getBusinessDirection) + .selectAs(CrmQuoteInfo::getQuoteStatus, CrmQuoteMaterialVo::getQuoteStatus) + .selectAs(CrmQuoteInfo::getPaymentMethod, CrmQuoteMaterialVo::getPaymentMethod) + .selectAs(CrmQuoteInfo::getCurrencyType, CrmQuoteMaterialVo::getCurrencyType) + .selectAs(CrmQuoteInfo::getTaxIncludedInfo, CrmQuoteMaterialVo::getTaxIncludedInfo) + .selectAs(CrmQuoteInfo::getTotalPrice, CrmQuoteMaterialVo::getTotalPrice) + .selectAs(CrmQuoteInfo::getTotalBeforeTax, CrmQuoteMaterialVo::getTotalBeforeTax) + .selectAs(CrmQuoteInfo::getTotalTax, CrmQuoteMaterialVo::getTotalTax) + .selectAs(CrmQuoteInfo::getTotalIncludingTax, CrmQuoteMaterialVo::getTotalIncludingTax) + // 明细页仍保留物料侧的联表信息: + // 因为业务导出和页面查看都需要直接识别 SAP 物料、销售物料,不能让用户再回主数据表二次查找。 .select(BaseMaterialInfo::getMaterialCode) .select(BaseMaterialInfo::getMaterialName) .select(BaseRelationMaterial::getSaleMaterialName) + .leftJoin(CrmQuoteInfo.class, CrmQuoteInfo::getQuoteId, CrmQuoteMaterial::getQuoteId) .leftJoin(BaseMaterialInfo.class, BaseMaterialInfo::getMaterialId, CrmQuoteMaterial::getMaterialId) .leftJoin(BaseRelationMaterial.class, BaseRelationMaterial::getRelationMaterialId, CrmQuoteMaterial::getRelationMaterialId) .eq(bo.getQuoteId() != null, CrmQuoteMaterial::getQuoteId, bo.getQuoteId()) + // 报价单号/名称保留模糊查询能力,便于从菜单进入全量明细页时按主表业务信息检索。 + .like(StringUtils.isNotBlank(bo.getQuoteCode()), CrmQuoteInfo::getQuoteCode, bo.getQuoteCode()) + .like(StringUtils.isNotBlank(bo.getQuoteName()), CrmQuoteInfo::getQuoteName, bo.getQuoteName()) .eq(bo.getItemNo() != null, CrmQuoteMaterial::getItemNo, bo.getItemNo()) .eq(StringUtils.isNotBlank(bo.getMaterialFlag()), CrmQuoteMaterial::getMaterialFlag, bo.getMaterialFlag()) .like(StringUtils.isNotBlank(bo.getProductName()), CrmQuoteMaterial::getProductName, bo.getProductName()) @@ -99,7 +121,8 @@ public class CrmQuoteMaterialServiceImpl implements ICrmQuoteMaterialService { .eq(bo.getIncludingPrice() != null, CrmQuoteMaterial::getIncludingPrice, bo.getIncludingPrice()) .eq(bo.getSubtotal() != null, CrmQuoteMaterial::getSubtotal, bo.getSubtotal()) .eq(StringUtils.isNotBlank(bo.getActiveFlag()), CrmQuoteMaterial::getActiveFlag, bo.getActiveFlag()) - // 导出/展示都按业务序号稳定排序,避免数组变量错位 + // 导出与页面列表都按业务序号稳定排序: + // 这样业务在线查看和线下导出的行顺序一致,便于逐行核对,不会出现“页面第3行导出变第5行”的错觉。 .orderByAsc(CrmQuoteMaterial::getItemNo) .orderByAsc(CrmQuoteMaterial::getQuoteMaterialId) ;