change(CrmQuoteMaterialVo): 完善报价单物料明细子表的查询与导出

dev
zangch@mesnac.com 19 hours ago
parent 118347c458
commit 5678e929fc

@ -32,6 +32,16 @@ public class CrmQuoteMaterialBo extends BaseEntity {
*/
private Long quoteId;
/**
*
*/
private String quoteCode;
/**
*
*/
private String quoteName;
/**
* (ERP)
*/

@ -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;

@ -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<CrmQuoteMaterial> oldItems = quoteMaterialMapper.selectList(
Wrappers.<CrmQuoteMaterial>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<BaseRelationMaterialVo> 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<BaseRelationMaterialVo> relationMaterials = baseRelationMaterialService.queryList(queryBo);
return relationMaterials == null || relationMaterials.isEmpty() ? null : relationMaterials.get(0);
}
/**
*
*

@ -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<CrmQuoteMaterial> 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)
;

Loading…
Cancel
Save