feat(crm): 增强报价单及物料管理,支持流程审批集成

- 在CrmQuoteInfo中新增客户名称字段,支持不落库显示
- 扩展CrmQuoteMaterial及对应Bo,添加标准物料标识和联表查询字段
- 优化报价单物料查询,联表带出物料编码、名称及销售物料名称
- 报价单Vo新增部门名称和客户名称展示字段
- 新增报价单提交接口,支持一键发起审批流程
- 集成远程工作流服务,支持流程状态变更事件监听与业务状态同步
- Mybatis-Plus XML映射文件更新,支持物料新字段及关联查询
- Controller层新增quoteSubmitAndFlowStart接口,增加权限校验和防重复提交
- Service层实现流程启动及流程事件处理,保证流程与业务状态一致
- 优化联表查询结构,提升报价单明细数据完整性与展示效果
dev
zangch@mesnac.com 4 weeks ago
parent 8932e839da
commit e9295a55a7

@ -91,6 +91,17 @@ public class CrmQuoteInfoController extends BaseController {
return toAjax(crmQuoteInfoService.insertByBo(bo));
}
/**
*
*/
@SaCheckPermission("oa/crm:crmQuoteInfo:add")
@Log(title = "报价单信息", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/quoteSubmitAndFlowStart")
public R<CrmQuoteInfoVo> quoteSubmitAndFlowStart(@Validated(AddGroup.class) @RequestBody CrmQuoteInfoBo bo) {
return R.ok(crmQuoteInfoService.quoteSubmitAndFlowStart(bo));
}
/**
*
*/
@ -204,7 +215,7 @@ public class CrmQuoteInfoController extends BaseController {
materialList.add(new QuoteTemplateMaterialDto());
}
// 方案A: 主表数据直接放入,占位符用{key}
// 主表数据直接放入,占位符用{key}
Map<String, Object> exportData = new LinkedHashMap<>();
// 将主表数据作为一个整体Map放入这样ExcelUtil遍历时会调用fill(map),从而匹配模板中的{key}
exportData.put("mainInfo", templateData);

@ -212,6 +212,12 @@ public class CrmQuoteInfo extends TenantEntity {
private String delFlag;
/**
* CrmCustomerInfo
*/
@TableField(exist = false)
private String customerName;
/**
* CrmCustomerContact
*/

@ -1,9 +1,6 @@
package org.dromara.oa.crm.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
@ -41,6 +38,11 @@ public class CrmQuoteMaterial extends TenantEntity {
*/
private Long itemNo;
/**
* 1 2
*/
private String materialFlag;
/**
* /
*/
@ -112,5 +114,22 @@ public class CrmQuoteMaterial extends TenantEntity {
@TableLogic
private String delFlag;
/**
* SAP base_material_info
*/
@TableField(exist = false)
private String materialCode;
/**
* SAP base_material_info
*/
@TableField(exist = false)
private String materialName;
/**
* base_relation_material
*/
@TableField(exist = false)
private String saleMaterialName;
}

@ -1,5 +1,6 @@
package org.dromara.oa.crm.domain.bo;
import cn.hutool.core.util.ObjectUtil;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -7,10 +8,10 @@ import lombok.EqualsAndHashCode;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.oa.crm.domain.CrmQuoteInfo;
import org.dromara.workflow.api.domain.RemoteFlowInstanceBizExt;
import java.util.Date;
import java.math.BigDecimal;
import java.util.List;
import java.util.*;
/**
* crm_quote_info
@ -209,4 +210,38 @@ public class CrmQuoteInfoBo extends BaseEntity {
*/
private List<CrmQuoteMaterialBo> itemsBo;
/**
*
*/
private String flowCode;
/**
* ( )
*/
private String handler;
/**
* {'entity': {}}
*/
private Map<String, Object> variables;
/**
*
*/
private RemoteFlowInstanceBizExt bizExt;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
public RemoteFlowInstanceBizExt getBizExt() {
if (ObjectUtil.isNull(bizExt)) {
bizExt = new RemoteFlowInstanceBizExt();
}
return bizExt;
}
}

@ -7,6 +7,7 @@ import lombok.EqualsAndHashCode;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.oa.crm.domain.CrmQuoteMaterial;
import java.math.BigDecimal;
/**
@ -36,6 +37,11 @@ public class CrmQuoteMaterialBo extends BaseEntity {
*/
private Long itemNo;
/**
* 1 2
*/
private String materialFlag;
/**
* /
*/

@ -81,6 +81,11 @@ public class CrmQuoteInfoVo implements Serializable {
@ExcelProperty(value = "部门")
private Long quoteDeptId;
/**
*
*/
private String deptName;
/**
*
*/
@ -176,6 +181,12 @@ public class CrmQuoteInfoVo implements Serializable {
@ExcelProperty(value = "客户方联系人ID")
private Long customerContactId;
/**
*
*/
@ExcelProperty(value = "客户名称")
private String customerName;
/**
*
*/

@ -1,14 +1,25 @@
package org.dromara.oa.crm.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.enums.OAStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
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.crm.domain.*;
import org.dromara.oa.crm.domain.bo.CrmQuoteInfoBo;
import org.dromara.oa.crm.domain.bo.CrmQuoteMaterialBo;
@ -19,6 +30,10 @@ import org.dromara.oa.crm.mapper.CrmQuoteInfoMapper;
import org.dromara.oa.crm.mapper.CrmQuoteMaterialMapper;
import org.dromara.oa.crm.service.ICrmCustomerContactService;
import org.dromara.oa.crm.service.ICrmQuoteInfoService;
import org.dromara.workflow.api.RemoteWorkflowService;
import org.dromara.workflow.api.domain.RemoteStartProcess;
import org.dromara.workflow.api.event.ProcessEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -35,12 +50,18 @@ import java.util.stream.Collectors;
*/
@RequiredArgsConstructor
@Service
@Slf4j
public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService {
//报价单主子表
private final CrmQuoteInfoMapper baseMapper;
private final CrmQuoteMaterialMapper quoteMaterialMapper;
//客户联系人
private final ICrmCustomerContactService customerContactService;
@DubboReference(timeout = 30000)
private RemoteWorkflowService remoteWorkflowService;
/**
*
*
@ -52,13 +73,20 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService {
CrmQuoteInfoVo vo = baseMapper.selectVoById(quoteId);
if (vo != null) {
// 关联查询子表明细
List<CrmQuoteMaterialVo> items = quoteMaterialMapper.selectVoList(
JoinWrappers.lambda(CrmQuoteMaterial.class)
.selectAll(CrmQuoteMaterial.class)
.eq("t.del_flag", "0")
.eq(CrmQuoteMaterial::getQuoteId, quoteId)
.orderByAsc(CrmQuoteMaterial::getItemNo)
);
MPJLambdaWrapper<CrmQuoteMaterial> lqw = JoinWrappers.lambda(CrmQuoteMaterial.class)
.selectAll(CrmQuoteMaterial.class)
.eq("t.del_flag", "0")
// 联表基础物料与销售物料,带出物料编码/名称等业务字段
.select(BaseMaterialInfo::getMaterialCode)
.select(BaseMaterialInfo::getMaterialName)
.select(BaseRelationMaterial::getSaleMaterialName)
.leftJoin(BaseMaterialInfo.class, BaseMaterialInfo::getMaterialId, CrmQuoteMaterial::getMaterialId)
.leftJoin(BaseRelationMaterial.class, BaseRelationMaterial::getRelationMaterialId, CrmQuoteMaterial::getRelationMaterialId)
.eq(CrmQuoteMaterial::getQuoteId, quoteId)
.orderByAsc(CrmQuoteMaterial::getItemNo);
// 直接使用 MPJ 的联表查询映射到 VO避免实体中 exist=false 字段无法通过 BaseMapperPlus 自动填充
List<CrmQuoteMaterialVo> items = quoteMaterialMapper.selectJoinList(CrmQuoteMaterialVo.class, lqw);
vo.setItemsVo(items);
}
return vo;
@ -100,6 +128,10 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService {
.selectAs("CustomerContact", CrmCustomerContact::getContactName, CrmQuoteInfo::getCustomerContactRealName)
.leftJoin(CrmCustomerContact.class, "CustomerContact", CrmCustomerContact::getContactId, CrmQuoteInfo::getCustomerContactId)
// 客户公司名称CrmCustomerInfo
.select(CrmCustomerInfo::getCustomerName)
.leftJoin(CrmCustomerInfo.class, CrmCustomerInfo::getCustomerId, CrmCustomerContact::getCustomerId)
/*
// 供货方联系人CrmCustomerContact别名SupplierContact
.selectAs("SupplierContact", CrmCustomerContact::getContactName, CrmQuoteInfo::getSupplierContactRealName)
@ -287,8 +319,10 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService {
// 别名连接与派生字段选择
.selectAs("CustomerContact", CrmCustomerContact::getContactName, CrmQuoteInfo::getCustomerContactRealName)
.leftJoin(CrmCustomerContact.class, "CustomerContact", CrmCustomerContact::getContactId, CrmQuoteInfo::getCustomerContactId)
// .leftJoin(CrmCustomerContact.class, "SupplierContact", CrmCustomerContact::getContactId, CrmQuoteInfo::getSupplierContactId)
// 客户公司名称
.select(CrmCustomerInfo::getCustomerName)
.leftJoin(CrmCustomerInfo.class, "Customer", CrmCustomerInfo::getCustomerId, CrmCustomerContact::getCustomerId)
// .leftJoin(CrmCustomerContact.class, "SupplierContact", CrmCustomerContact::getContactId, CrmQuoteInfo::getSupplierContactId)
//供应商信息
.select(CrmSupplierInfo::getSupplierName)
.leftJoin(CrmSupplierInfo.class, CrmSupplierInfo::getSupplierId, CrmQuoteInfo::getSupplierContactId)
@ -345,4 +379,76 @@ public class CrmQuoteInfoServiceImpl implements ICrmQuoteInfoService {
update.setTotalPrice(totalPrice.setScale(2, RoundingMode.HALF_UP));
return baseMapper.updateById(update) > 0;
}
/**
*
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public CrmQuoteInfoVo quoteSubmitAndFlowStart(CrmQuoteInfoBo bo) {
// 先按当前表单数据进行保存(主子表统一处理)
if (bo.getQuoteId() == null) {
this.insertByBo(bo);
} else {
this.updateByBo(bo);
}
if (bo.getQuoteId() == null) {
throw new ServiceException("报价单ID为空无法发起流程");
}
// 后端发起流程需忽略节点权限
bo.getVariables().put("ignore", true);
RemoteStartProcess startProcess = new RemoteStartProcess();
startProcess.setBusinessId(bo.getQuoteId().toString());
startProcess.setFlowCode(bo.getFlowCode());
startProcess.setVariables(bo.getVariables());
startProcess.setBizExt(bo.getBizExt());
bo.getBizExt().setBusinessId(startProcess.getBusinessId());
boolean flag = remoteWorkflowService.startCompleteTask(startProcess);
if (!flag) {
throw new ServiceException("流程发起异常");
}
// 返回最新数据
return this.queryById(bo.getQuoteId());
}
/**
*
*/
@EventListener(condition = "#processEvent.flowCode =='OACQ'")
public void processHandler(ProcessEvent processEvent) {
TenantHelper.dynamic(processEvent.getTenantId(), () -> {
log.info("报价单流程回调: {}", processEvent);
CrmQuoteInfo quoteInfo = baseMapper.selectById(Convert.toLong(processEvent.getBusinessId()));
if (quoteInfo == null) {
return;
}
// 同步流程状态
quoteInfo.setFlowStatus(processEvent.getStatus());
Map<String, Object> params = processEvent.getParams();
if (MapUtil.isNotEmpty(params)) {
// 预留:当前节点办理人
String handler = Convert.toStr(params.get("handler"));
}
// 按通用规范映射业务状态
if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.WAITING.getStatus())) {
quoteInfo.setQuoteStatus(OAStatusEnum.APPROVING.getStatus());
} else if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.FINISH.getStatus())) {
quoteInfo.setQuoteStatus(OAStatusEnum.COMPLETED.getStatus());
} else if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.INVALID.getStatus())
|| Objects.equals(processEvent.getStatus(), BusinessStatusEnum.TERMINATION.getStatus())) {
quoteInfo.setQuoteStatus(OAStatusEnum.INVALID.getStatus());
} else if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.BACK.getStatus())
|| Objects.equals(processEvent.getStatus(), BusinessStatusEnum.CANCEL.getStatus())) {
// 退回或撤销回到草稿
quoteInfo.setQuoteStatus(OAStatusEnum.DRAFT.getStatus());
}
baseMapper.updateById(quoteInfo);
});
}
}

@ -73,16 +73,18 @@ public class CrmQuoteMaterialServiceImpl implements ICrmQuoteMaterialService {
private MPJLambdaWrapper<CrmQuoteMaterial> buildQueryWrapper(CrmQuoteMaterialBo bo) {
Map<String, Object> params = bo.getParams();
MPJLambdaWrapper<CrmQuoteMaterial> lqw = JoinWrappers.lambda(CrmQuoteMaterial.class)
.selectAll(CrmQuoteMaterial.class)
.eq(CrmQuoteMaterial::getDelFlag, "0")
// 联表选择SAP物料名称 & 销售物料名称
.select(BaseMaterialInfo::getMaterialName)
.select(BaseRelationMaterial::getSaleMaterialName)
.leftJoin(BaseMaterialInfo.class, BaseMaterialInfo::getMaterialId, CrmQuoteMaterial::getMaterialId)
.leftJoin(BaseRelationMaterial.class, BaseRelationMaterial::getRelationMaterialId, CrmQuoteMaterial::getRelationMaterialId)
.selectAll(CrmQuoteMaterial.class)
.eq(CrmQuoteMaterial::getDelFlag, "0")
// 联表选择SAP物料编码/名称、销售物料名称、计量单位名称
.select(BaseMaterialInfo::getMaterialCode)
.select(BaseMaterialInfo::getMaterialName)
.select(BaseRelationMaterial::getSaleMaterialName)
.leftJoin(BaseMaterialInfo.class, BaseMaterialInfo::getMaterialId, CrmQuoteMaterial::getMaterialId)
.leftJoin(BaseRelationMaterial.class, BaseRelationMaterial::getRelationMaterialId, CrmQuoteMaterial::getRelationMaterialId)
.eq(bo.getQuoteId() != null, CrmQuoteMaterial::getQuoteId, bo.getQuoteId())
.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())
.eq(StringUtils.isNotBlank(bo.getSpecificationDescription()), CrmQuoteMaterial::getSpecificationDescription, bo.getSpecificationDescription())
.eq(bo.getMaterialId() != null, CrmQuoteMaterial::getMaterialId, bo.getMaterialId())

@ -13,10 +13,12 @@
t.total_tax, t.total_including_tax, t.customer_contact_id, t.customer_contact_name, t.customer_contact_phone, t.customer_contact_email,
t.supplier_contact_id, t.supplier_contact_name, t.supplier_contact_phone, t.supplier_contact_email, t.project_id, t.template_id, t.oss_id, t.quote_status,
t.flow_status, t.remark,
d.dept_name as deptName,
u3.nick_name as createName,
t.del_flag, t.create_dept, t.create_by, t.create_time, t.update_by, t.update_time
from crm_quote_info t
left join sys_user u3 on t.create_by = u3.user_id
left join sys_dept d on d.dept_id = t.quote_dept_id
${ew.getCustomSqlSegment}
</select>

@ -7,20 +7,30 @@
</resultMap>
<select id="selectCustomCrmQuoteMaterialVoList" resultMap="CrmQuoteMaterialResult">
select quote_material_id, tenant_id, quote_id, item_no, product_name, specification_description, material_id, relation_material_id, amount, unit_id, unit_name, before_price, tax_rate, including_price, subtotal, remark, active_flag, del_flag, create_dept, create_by, create_time, update_by, update_time from crm_quote_material t
select t.quote_material_id, t.tenant_id, t.quote_id, t.item_no, t.material_flag, t.product_name, t.specification_description,
t.material_id, t.relation_material_id, t.amount, t.unit_id, t.unit_name, t.before_price, t.tax_rate,
t.including_price, t.subtotal, t.remark, t.active_flag, t.del_flag, t.create_dept, t.create_by,
t.create_time, t.update_by, t.update_time
from crm_quote_material t
${ew.getCustomSqlSegment}
</select>
<!-- 根据ID查询详情 -->
<select id="selectCustomCrmQuoteMaterialVoById" resultMap="CrmQuoteMaterialResult">
select quote_material_id, tenant_id, quote_id, item_no, product_name, specification_description, material_id, relation_material_id, amount, unit_id, unit_name, before_price, tax_rate, including_price, subtotal, remark, active_flag, del_flag, create_dept, create_by, create_time, update_by, update_time
select t.quote_material_id, t.tenant_id, t.quote_id, t.item_no, t.material_flag, t.product_name, t.specification_description,
t.material_id, t.relation_material_id, t.amount, t.unit_id, t.unit_name, t.before_price, t.tax_rate,
t.including_price, t.subtotal, t.remark, t.active_flag, t.del_flag, t.create_dept, t.create_by,
t.create_time, t.update_by, t.update_time
from crm_quote_material t
where t.quote_material_id = #{quoteMaterialId}
</select>
<!-- 批量查询 - 根据ID列表 -->
<select id="selectCustomCrmQuoteMaterialVoByIds" resultMap="CrmQuoteMaterialResult">
select quote_material_id, tenant_id, quote_id, item_no, product_name, specification_description, material_id, relation_material_id, amount, unit_id, unit_name, before_price, tax_rate, including_price, subtotal, remark, active_flag, del_flag, create_dept, create_by, create_time, update_by, update_time
select t.quote_material_id, t.tenant_id, t.quote_id, t.item_no, t.material_flag, t.product_name, t.specification_description,
t.material_id, t.relation_material_id, t.amount, t.unit_id, t.unit_name, t.before_price, t.tax_rate,
t.including_price, t.subtotal, t.remark, t.active_flag, t.del_flag, t.create_dept, t.create_by,
t.create_time, t.update_by, t.update_time
from crm_quote_material t
where t.quote_material_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
@ -36,7 +46,10 @@
<!-- 分页查询(带自定义条件) -->
<select id="selectCustomCrmQuoteMaterialVoPage" resultMap="CrmQuoteMaterialResult">
select quote_material_id, tenant_id, quote_id, item_no, product_name, specification_description, material_id, relation_material_id, amount, unit_id, unit_name, before_price, tax_rate, including_price, subtotal, remark, active_flag, del_flag, create_dept, create_by, create_time, update_by, update_time
select t.quote_material_id, t.tenant_id, t.quote_id, t.item_no, t.material_flag, t.product_name, t.specification_description,
t.material_id, t.relation_material_id, t.amount, t.unit_id, t.unit_name, t.before_price, t.tax_rate,
t.including_price, t.subtotal, t.remark, t.active_flag, t.del_flag, t.create_dept, t.create_by,
t.create_time, t.update_by, t.update_time
from crm_quote_material t
${ew.getCustomSqlSegment}
</select>
@ -50,6 +63,8 @@
item_no,
material_flag,
product_name,
specification_description,
@ -98,6 +113,8 @@
#{item.itemNo},
#{item.materialFlag},
#{item.productName},
#{item.specificationDescription},
@ -154,6 +171,9 @@
<if test="item.itemNo != null">
item_no = #{item.itemNo},
</if>
<if test="item.materialFlag != null and item.materialFlag != ''">
material_flag = #{item.materialFlag},
</if>
<if test="item.productName != null and item.productName != ''">
product_name = #{item.productName},
</if>
@ -224,8 +244,8 @@
<!-- 根据ID列表批量删除 -->
<delete id="deleteCustomCrmQuoteMaterialByIds">
delete from crm_quote_material
where quote_material_id in
delete from crm_quote_material t
where t.quote_material_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>

Loading…
Cancel
Save