|
|
|
|
@ -1,44 +1,49 @@
|
|
|
|
|
package org.dromara.oa.erp.service.impl;
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.bean.BeanUtil;
|
|
|
|
|
import cn.hutool.core.convert.Convert;
|
|
|
|
|
import cn.hutool.core.convert.NumberChineseFormatter;
|
|
|
|
|
import cn.hutool.core.map.MapUtil;
|
|
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
import com.deepoove.poi.data.RowRenderData;
|
|
|
|
|
import com.deepoove.poi.data.Rows;
|
|
|
|
|
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.DateUtils;
|
|
|
|
|
import org.dromara.common.core.utils.MapstructUtils;
|
|
|
|
|
import org.dromara.common.core.utils.StringUtils;
|
|
|
|
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
|
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
|
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
import com.github.yulichang.toolkit.JoinWrappers;
|
|
|
|
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
|
|
import org.dromara.common.tenant.helper.TenantHelper;
|
|
|
|
|
import org.dromara.oa.base.domain.vo.BaseRelationMaterialVo;
|
|
|
|
|
import org.dromara.oa.erp.domain.ErpContractMaterial;
|
|
|
|
|
import org.dromara.oa.erp.domain.ErpContractPaymentMethod;
|
|
|
|
|
import org.dromara.oa.erp.domain.vo.ErpContractMaterialVo;
|
|
|
|
|
import org.dromara.oa.erp.domain.vo.ErpContractPaymentMethodVo;
|
|
|
|
|
import org.dromara.oa.erp.mapper.ErpContractMaterialMapper;
|
|
|
|
|
import org.dromara.oa.erp.mapper.ErpContractPaymentMethodMapper;
|
|
|
|
|
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.vo.CrmPaymentAccountVo;
|
|
|
|
|
import org.dromara.oa.crm.service.ICrmPaymentAccountService;
|
|
|
|
|
import org.dromara.oa.erp.domain.ErpContractInfo;
|
|
|
|
|
import org.dromara.oa.erp.domain.ErpContractMaterial;
|
|
|
|
|
import org.dromara.oa.erp.domain.ErpContractPaymentMethod;
|
|
|
|
|
import org.dromara.oa.erp.domain.bo.ErpContractInfoBo;
|
|
|
|
|
import org.dromara.oa.erp.domain.vo.ErpContractInfoVo;
|
|
|
|
|
import org.dromara.oa.erp.domain.vo.ErpContractMaterialVo;
|
|
|
|
|
import org.dromara.oa.erp.domain.vo.ErpContractPaymentMethodVo;
|
|
|
|
|
import org.dromara.oa.erp.mapper.ErpContractInfoMapper;
|
|
|
|
|
import org.dromara.oa.erp.mapper.ErpContractMaterialMapper;
|
|
|
|
|
import org.dromara.oa.erp.mapper.ErpContractPaymentMethodMapper;
|
|
|
|
|
import org.dromara.oa.erp.service.IErpContractInfoService;
|
|
|
|
|
import org.dromara.system.api.RemoteUserService;
|
|
|
|
|
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.dromara.oa.erp.domain.bo.ErpContractInfoBo;
|
|
|
|
|
import org.dromara.oa.erp.domain.vo.ErpContractInfoVo;
|
|
|
|
|
import org.dromara.oa.erp.domain.ErpContractInfo;
|
|
|
|
|
import org.dromara.oa.erp.mapper.ErpContractInfoMapper;
|
|
|
|
|
import org.dromara.oa.erp.service.IErpContractInfoService;
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
@ -55,6 +60,12 @@ import java.util.stream.Collectors;
|
|
|
|
|
@Slf4j
|
|
|
|
|
public class ErpContractInfoServiceImpl implements IErpContractInfoService {
|
|
|
|
|
|
|
|
|
|
private static final String WORD_DATE_FORMAT = "yyyy年MM月dd日";
|
|
|
|
|
|
|
|
|
|
private static final String BUSINESS_DIRECTION_LEADER_NODE_NAME = "业务方向负责人";
|
|
|
|
|
|
|
|
|
|
private static final String FINANCE_MANAGER_NODE_NAME = "财务经理";
|
|
|
|
|
|
|
|
|
|
private final ErpContractInfoMapper baseMapper;
|
|
|
|
|
|
|
|
|
|
private final ErpContractMaterialMapper contractMaterialMapper;
|
|
|
|
|
@ -65,6 +76,8 @@ public class ErpContractInfoServiceImpl implements IErpContractInfoService {
|
|
|
|
|
|
|
|
|
|
private final ICrmPaymentAccountService crmPaymentAccountService;
|
|
|
|
|
|
|
|
|
|
private final RemoteUserService remoteUserService;
|
|
|
|
|
|
|
|
|
|
@DubboReference(timeout = 30000)
|
|
|
|
|
private RemoteWorkflowService remoteWorkflowService;
|
|
|
|
|
|
|
|
|
|
@ -395,6 +408,215 @@ public class ErpContractInfoServiceImpl implements IErpContractInfoService {
|
|
|
|
|
return baseMapper.deleteByIds(ids) > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 组装合同审批单Word导出数据
|
|
|
|
|
*
|
|
|
|
|
* @param contractId 合同ID
|
|
|
|
|
* @return Word模板数据Map
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public Map<String, Object> buildApprovalWordExportData(Long contractId) {
|
|
|
|
|
ErpContractInfoVo contractInfo = queryById(contractId);
|
|
|
|
|
if (contractInfo == null) {
|
|
|
|
|
throw new ServiceException("合同不存在,ID:" + contractId);
|
|
|
|
|
}
|
|
|
|
|
if (!OAStatusEnum.COMPLETED.getStatus().equals(contractInfo.getContractStatus())) {
|
|
|
|
|
throw new ServiceException("仅合同状态为“可用”时允许导出审批单");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Map<String, Object> data = new HashMap<>(64);
|
|
|
|
|
// 审批单导出优先保障“合同基础信息”完整,避免审批记录正确但主表关键信息缺失导致单据不可用
|
|
|
|
|
data.put("合同编号", strVal(contractInfo.getContractCode()));
|
|
|
|
|
data.put("合同名称", strVal(contractInfo.getContractName()));
|
|
|
|
|
data.put("客户合同编号", strVal(contractInfo.getCustomerContractCode()));
|
|
|
|
|
data.put("合同签订日期", formatWordDate(contractInfo.getContractDate()));
|
|
|
|
|
data.put("合同总价", contractInfo.getTotalPrice() == null ? "" : contractInfo.getTotalPrice().toPlainString());
|
|
|
|
|
data.put("甲方公司", strVal(contractInfo.getOneCustomerName()));
|
|
|
|
|
data.put("乙方公司", strVal(contractInfo.getTwoCustomerName()));
|
|
|
|
|
data.put("合同负责人", strVal(contractInfo.getContractManagerName()));
|
|
|
|
|
data.put("所属部门", strVal(contractInfo.getDeptName()));
|
|
|
|
|
data.put("付款方式", strVal(contractInfo.getPaymentMethod()));
|
|
|
|
|
data.put("付款账户信息", strVal(contractInfo.getPaymentAccountInfo()));
|
|
|
|
|
data.put("合同类型", strVal(contractInfo.getContractType()));
|
|
|
|
|
data.put("合同大类", strVal(contractInfo.getContractCategory()));
|
|
|
|
|
data.put("业务方向", strVal(contractInfo.getBusinessDirection()));
|
|
|
|
|
data.put("签订地点", strVal(contractInfo.getSigningPlace()));
|
|
|
|
|
data.put("备注", strVal(contractInfo.getRemark()));
|
|
|
|
|
data.put("内部合同号", strVal(contractInfo.getInternalContractCode()));
|
|
|
|
|
data.put("外部合同号", strVal(contractInfo.getExternalContractCode()));
|
|
|
|
|
data.put("订单号", strVal(contractInfo.getOrderContractCode()));
|
|
|
|
|
data.put("项目号", strVal(contractInfo.getProjectContractCode()));
|
|
|
|
|
// Why:当前业务已明确“交付启动期限”与“交货期”同义,双键同时输出可兼容不同模板占位符命名
|
|
|
|
|
String deliveryPeriod = strVal(contractInfo.getDeliveryStart());
|
|
|
|
|
data.put("交货期", deliveryPeriod);
|
|
|
|
|
data.put("交付启动期限", deliveryPeriod);
|
|
|
|
|
data.put("质保期", strVal(contractInfo.getWarrantyPeriod()));
|
|
|
|
|
data.put("质保期描述", strVal(contractInfo.getWarrantyPeriodDescription()));
|
|
|
|
|
|
|
|
|
|
// 按需求先置空的字段
|
|
|
|
|
data.put("行业/大区", "");
|
|
|
|
|
data.put("是否为销售合同标准文本", "");
|
|
|
|
|
data.put("是否经法务部备案的销售合同文本", "");
|
|
|
|
|
data.put("销售产品是否出口", "");
|
|
|
|
|
data.put("盖章要求", "");
|
|
|
|
|
data.put("合同争议解决方式", "");
|
|
|
|
|
|
|
|
|
|
// 审批单只展示“已办节点”,避免把待办节点(无审批时间/意见)混入导出文档
|
|
|
|
|
Map<String, Object> flowData = remoteWorkflowService.flowHisTaskList(String.valueOf(contractId));
|
|
|
|
|
List<Map<String, Object>> handledTasks = extractHandledTasks(flowData == null ? null : flowData.get("list"));
|
|
|
|
|
// Why:固定审批区块要按流程节点名精确落位,避免后续流程扩展后把别的节点意见写错位置
|
|
|
|
|
fillApprovalNodeData(data, handledTasks, BUSINESS_DIRECTION_LEADER_NODE_NAME, "业务方向负责人");
|
|
|
|
|
fillApprovalNodeData(data, handledTasks, FINANCE_MANAGER_NODE_NAME, "财务经理");
|
|
|
|
|
List<RowRenderData> approvalRows = new ArrayList<>();
|
|
|
|
|
int seq = 1;
|
|
|
|
|
for (Map<String, Object> task : handledTasks) {
|
|
|
|
|
String approveName = resolveApproveName(task);
|
|
|
|
|
String message = strVal(task.get("message"));
|
|
|
|
|
Date updateTime = parseDate(task.get("updateTime"));
|
|
|
|
|
String approvalTime = formatWordDate(updateTime);
|
|
|
|
|
approvalRows.add(Rows.of(
|
|
|
|
|
String.valueOf(seq++),
|
|
|
|
|
approveName,
|
|
|
|
|
message,
|
|
|
|
|
approvalTime
|
|
|
|
|
).create());
|
|
|
|
|
}
|
|
|
|
|
data.put("审批记录表", approvalRows);
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按节点名称提取固定审批区块数据
|
|
|
|
|
*/
|
|
|
|
|
private void fillApprovalNodeData(Map<String, Object> data, List<Map<String, Object>> handledTasks,
|
|
|
|
|
String nodeName, String keyPrefix) {
|
|
|
|
|
Map<String, Object> task = findLatestTaskByNodeName(handledTasks, nodeName);
|
|
|
|
|
if (task == null) {
|
|
|
|
|
data.put(keyPrefix + "审批人", "");
|
|
|
|
|
data.put(keyPrefix + "审批意见", "");
|
|
|
|
|
data.put(keyPrefix + "审批时间", "");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.put(keyPrefix + "审批人", resolveApproveName(task));
|
|
|
|
|
data.put(keyPrefix + "审批意见", strVal(task.get("message")));
|
|
|
|
|
data.put(keyPrefix + "审批时间", formatWordDate(parseDate(task.get("updateTime"))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 提取已办审批节点,并按完成时间升序排序
|
|
|
|
|
*/
|
|
|
|
|
private List<Map<String, Object>> extractHandledTasks(Object listObj) {
|
|
|
|
|
if (!(listObj instanceof List<?> taskList) || taskList.isEmpty()) {
|
|
|
|
|
return Collections.emptyList();
|
|
|
|
|
}
|
|
|
|
|
List<Map<String, Object>> handledTasks = new ArrayList<>();
|
|
|
|
|
for (Object taskObj : taskList) {
|
|
|
|
|
Map<String, Object> taskMap = toMap(taskObj);
|
|
|
|
|
if (taskMap.isEmpty()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (taskMap.get("updateTime") != null) {
|
|
|
|
|
handledTasks.add(taskMap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
handledTasks.sort(Comparator.comparing(task -> {
|
|
|
|
|
Date updateTime = parseDate(task.get("updateTime"));
|
|
|
|
|
return updateTime == null ? new Date(0L) : updateTime;
|
|
|
|
|
}));
|
|
|
|
|
return handledTasks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从已办节点中找到指定节点的最后一次审批记录
|
|
|
|
|
*/
|
|
|
|
|
private Map<String, Object> findLatestTaskByNodeName(List<Map<String, Object>> handledTasks, String nodeName) {
|
|
|
|
|
if (StringUtils.isBlank(nodeName) || handledTasks == null || handledTasks.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
for (int i = handledTasks.size() - 1; i >= 0; i--) {
|
|
|
|
|
Map<String, Object> task = handledTasks.get(i);
|
|
|
|
|
// Why:不同工作流版本可能返回 nodeName / targetNodeName,双字段兼容可以减少后续接口改动带来的模板失效
|
|
|
|
|
String taskNodeName = strVal(task.get("nodeName"));
|
|
|
|
|
String targetNodeName = strVal(task.get("targetNodeName"));
|
|
|
|
|
if (nodeName.equals(taskNodeName) || nodeName.equals(targetNodeName)) {
|
|
|
|
|
return task;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析审批人显示名称
|
|
|
|
|
*/
|
|
|
|
|
private String resolveApproveName(Map<String, Object> task) {
|
|
|
|
|
String approveName = strVal(task.get("approveName"));
|
|
|
|
|
if (StringUtils.isNotBlank(approveName)) {
|
|
|
|
|
return approveName;
|
|
|
|
|
}
|
|
|
|
|
String approver = strVal(task.get("approver"));
|
|
|
|
|
if (StringUtils.isBlank(approver)) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Why:Dubbo 导出链路返回的是纯 Map 后,工作流里的 @Translation 不再生效,这里补一层用户昵称查询保证导出给业务人员的是可读姓名
|
|
|
|
|
String nickname = remoteUserService.selectNicknameByIds(approver);
|
|
|
|
|
return StringUtils.isNotBlank(nickname) ? nickname : approver;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("审批人昵称解析失败, approver={}", approver, e);
|
|
|
|
|
return approver;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<String, Object> toMap(Object obj) {
|
|
|
|
|
if (obj == null) {
|
|
|
|
|
return Collections.emptyMap();
|
|
|
|
|
}
|
|
|
|
|
if (obj instanceof Map<?, ?> rawMap) {
|
|
|
|
|
Map<String, Object> result = new HashMap<>(rawMap.size());
|
|
|
|
|
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
|
|
|
|
|
result.put(String.valueOf(entry.getKey()), entry.getValue());
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return BeanUtil.beanToMap(obj);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Date parseDate(Object rawDate) {
|
|
|
|
|
if (rawDate == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (rawDate instanceof Date date) {
|
|
|
|
|
return date;
|
|
|
|
|
}
|
|
|
|
|
if (rawDate instanceof Number number) {
|
|
|
|
|
long millis = number.longValue();
|
|
|
|
|
if (String.valueOf(millis).length() == 10) {
|
|
|
|
|
millis = millis * 1000;
|
|
|
|
|
}
|
|
|
|
|
return new Date(millis);
|
|
|
|
|
}
|
|
|
|
|
return DateUtils.parseDate(rawDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String formatDateTime(Date date, String format) {
|
|
|
|
|
if (date == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
return DateUtils.parseDateToStr(format, date);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String formatWordDate(Date date) {
|
|
|
|
|
return formatDateTime(date, WORD_DATE_FORMAT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String strVal(Object value) {
|
|
|
|
|
if (value == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
return String.valueOf(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 提交合同信息并提交流程
|
|
|
|
|
*
|
|
|
|
|
|