package cn.iocoder.yudao.module.crm.service.invoice;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.ListUtil;
|
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
|
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
|
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
|
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
|
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceEntryReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceImportExcelVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceImportRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoicePageReqVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaleOutRespVO;
|
import cn.iocoder.yudao.module.crm.controller.admin.invoice.vo.invoice.CrmInvoiceSaveReqVO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.invoice.CrmInvoiceDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO;
|
import cn.iocoder.yudao.module.crm.dal.mysql.invoice.CrmInvoiceMapper;
|
import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivablePlanMapper;
|
import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
|
import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
|
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
|
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
|
import cn.iocoder.yudao.module.crm.enums.receivable.CrmPaymentStageEnum;
|
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
|
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
|
import cn.iocoder.yudao.module.mes.api.productsales.MesWmProductSalesApi;
|
import cn.iocoder.yudao.module.mes.api.productsales.dto.MesWmProductSalesRespDTO;
|
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
|
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
|
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
|
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
|
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
import com.mzt.logapi.context.LogRecordContext;
|
import com.mzt.logapi.service.impl.DiffParseFunction;
|
import com.mzt.logapi.starter.annotation.LogRecord;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Collection;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.CRM_INVOICE_TYPE;
|
import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
|
import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;
|
import static cn.iocoder.yudao.module.crm.util.CrmAuditStatusUtils.convertBpmResultToAuditStatus;
|
|
/**
|
* CRM 开票 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class CrmInvoiceServiceImpl implements CrmInvoiceService {
|
|
/**
|
* BPM 开票审批流程标识
|
*/
|
public static final String BPM_PROCESS_DEFINITION_KEY = "crm-invoice-audit";
|
|
/**
|
* 发票附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#CRM_INVOICE}
|
*/
|
private static final String INVOICE_RECORD_TYPE = "crm_invoice";
|
|
/**
|
* ERP 销售订单审核通过状态,对应 cn.iocoder.yudao.module.erp.enums.ErpAuditStatus#APPROVE
|
*/
|
private static final Integer ERP_SALE_ORDER_STATUS_APPROVE = 20;
|
|
/**
|
* MES 销售出库单已完成状态,对应 cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum#FINISHED
|
*/
|
private static final Integer MES_PRODUCT_SALES_STATUS_FINISHED = 4;
|
|
/**
|
* MES 销售出库单已取消状态
|
*/
|
private static final Integer MES_PRODUCT_SALES_STATUS_CANCELED = 5;
|
|
/**
|
* 开票占用额度的审批状态:审批中 + 审批通过
|
*/
|
private static final List<Integer> INVOICE_OCCUPY_STATUSES = List.of(
|
CrmAuditStatusEnum.PROCESS.getStatus(), CrmAuditStatusEnum.APPROVE.getStatus());
|
|
@Resource
|
private CrmInvoiceMapper invoiceMapper;
|
@Resource
|
private CrmReceivablePlanMapper receivablePlanMapper;
|
|
@Resource
|
private ErpSaleOrderApi saleOrderApi;
|
@Resource
|
private MesWmProductSalesApi productSalesApi;
|
|
@Resource
|
private CrmNoRedisDAO noRedisDAO;
|
|
@Resource
|
private CrmContractService contractService;
|
@Resource
|
private CrmPermissionService permissionService;
|
|
@Resource
|
private AdminUserApi adminUserApi;
|
@Resource
|
private BpmProcessInstanceApi bpmProcessInstanceApi;
|
@Resource
|
private StorageAttachmentApi storageAttachmentApi;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_CREATE_SUB_TYPE, bizNo = "{{#invoice.id}}",
|
success = CRM_INVOICE_CREATE_SUCCESS)
|
public Long createInvoice(CrmInvoiceSaveReqVO createReqVO) {
|
// 1.1 校验关联数据存在(合同 -> 客户)
|
validateRelationDataExists(createReqVO);
|
// 1.2 校验销售订单、销售出库单,并回填出库单号
|
validateSaleOrderAndOut(createReqVO);
|
// 1.3 校验可开票金额超过上限
|
validateInvoicePriceExceedsLimit(createReqVO);
|
// 1.4 生成开票编号
|
String no = noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
|
if (invoiceMapper.selectByNo(no) != null) {
|
throw exception(INVOICE_NO_EXISTS);
|
}
|
|
// 2.1 插入开票:已填写发票号码视为已录入发票
|
CrmInvoiceDO invoice = BeanUtils.toBean(createReqVO, CrmInvoiceDO.class)
|
.setNo(no).setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
|
applyEntryStatus(invoice, createReqVO.getInvoiceNo(), null);
|
invoiceMapper.insert(invoice);
|
|
// 2.2 绑定发票附件
|
if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
|
storageAttachmentApi.bindAttachments("file", INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds());
|
}
|
|
// 2.3 同步收款节点(合同款/预付款/到货款/验收款)
|
syncPaymentNodes(invoice, createReqVO.getPaymentNodes());
|
|
// 3. 创建数据权限
|
permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_INVOICE.getType())
|
.setBizId(invoice.getId()).setUserId(createReqVO.getOwnerUserId())
|
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); // 设置当前操作的人为负责人
|
|
// 4. 记录操作日志上下文
|
LogRecordContext.putVariable("invoice", invoice);
|
return invoice.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_IMPORT_SUB_TYPE, bizNo = "0",
|
success = CRM_INVOICE_IMPORT_SUCCESS)
|
public CrmInvoiceImportRespVO importInvoiceList(List<CrmInvoiceImportExcelVO> importList) {
|
CrmInvoiceImportRespVO respVO = CrmInvoiceImportRespVO.builder()
|
.createList(new ArrayList<>())
|
.failureList(new LinkedHashMap<>())
|
.build();
|
if (CollUtil.isEmpty(importList)) {
|
// 空文件也要给日志文案兜底,避免 {{#importSummary}} 取不到值
|
OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList());
|
return respVO;
|
}
|
|
Map<String, Integer> firstRowNoByKey = new HashMap<>();
|
for (int i = 0; i < importList.size(); i++) {
|
CrmInvoiceImportExcelVO row = importList.get(i);
|
if (isImportRowEmpty(row)) {
|
continue; // 跳过空行
|
}
|
String label = StrUtil.isNotBlank(row.getInvoiceNo()) ? "发票号码 " + row.getInvoiceNo().trim()
|
: "第 " + (i + 2) + " 行";
|
String importKey = buildInvoiceImportKey(row);
|
try {
|
Integer firstRowNo = firstRowNoByKey.get(importKey);
|
if (firstRowNo != null) {
|
throw duplicateRowException(firstRowNo, buildInvoiceKeyDesc(row));
|
}
|
respVO.getCreateList().add(createInvoiceByImport(row));
|
firstRowNoByKey.putIfAbsent(importKey, i + 2);
|
} catch (Exception ex) {
|
respVO.getFailureList().put(label, resolveImportErrorMessage(ex));
|
}
|
}
|
// 记录导入结果汇总,供 @LogRecord 的 success 文案引用(开票导入无「更新」分支,故传 null)
|
OperateLogUtils.putImportSummary(respVO.getCreateList(), null, respVO.getFailureList());
|
return respVO;
|
}
|
|
private boolean isImportRowEmpty(CrmInvoiceImportExcelVO row) {
|
return row == null || (StrUtil.isBlank(row.getNo()) && StrUtil.isBlank(row.getContractNo())
|
&& StrUtil.isBlank(row.getInvoiceType()) && StrUtil.isBlank(row.getInvoiceNo())
|
&& StrUtil.isBlank(row.getInvoiceTitle()) && row.getPrice() == null
|
&& StrUtil.isBlank(row.getInvoiceTime()) && StrUtil.isBlank(row.getRemark()));
|
}
|
|
/**
|
* 依据导入数据创建一张开票。客户取合同所属客户;负责人=当前登录用户;审批状态=未提交。
|
*/
|
private String createInvoiceByImport(CrmInvoiceImportExcelVO row) {
|
// 1. 合同校验
|
if (StrUtil.isBlank(row.getContractNo())) {
|
throw new IllegalArgumentException("合同编号不能为空:请填写模板中该行的「合同编号」列");
|
}
|
CrmContractDO contract = contractService.getContractByNo(row.getContractNo().trim());
|
if (contract == null) {
|
throw new IllegalArgumentException("合同编号不存在:" + row.getContractNo().trim()
|
+ "。请先在「CRM 合同管理」中创建或导入该合同(编号需与模板完全一致),或修改模板中的合同编号后重新导入");
|
}
|
if (ObjUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
|
throw new IllegalArgumentException("合同[" + contract.getNo() + "]当前状态为「"
|
+ CrmAuditStatusEnum.getNameByStatus(contract.getAuditStatus()) + "」,只有「审核通过」的合同才能开票。"
|
+ "请先在「CRM 合同管理」中完成该合同审批,或改用其他已审核通过的合同编号后重新导入");
|
}
|
|
// 2. 开票金额校验
|
if (row.getPrice() == null) {
|
throw new IllegalArgumentException("开票金额不能为空:请填写模板中该行的「开票金额(元)」列");
|
}
|
if (row.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
throw new IllegalArgumentException("开票金额必须大于 0,当前为 "
|
+ row.getPrice().stripTrailingZeros().toPlainString() + "。请修改模板中的「开票金额(元)」后重新导入");
|
}
|
BigDecimal remainingInvoicePrice = getRemainingInvoicePrice(contract, null);
|
if (row.getPrice().compareTo(remainingInvoicePrice) > 0) {
|
throw new IllegalArgumentException("开票金额超过合同剩余可开票金额:合同[" + contract.getNo()
|
+ "]剩余可开票金额 " + remainingInvoicePrice.stripTrailingZeros().toPlainString() + " 元,本次导入金额 "
|
+ row.getPrice().stripTrailingZeros().toPlainString()
|
+ " 元。请调整模板中的「开票金额(元)」,或先在系统中核对已开票金额后重新导入");
|
}
|
|
// 3. 发票类型校验
|
Integer invoiceType = resolveInvoiceType(row.getInvoiceType());
|
|
// 4. 开票编号:填写则使用,留空则自动生成
|
String no = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim()
|
: noRedisDAO.generate(CrmNoRedisDAO.INVOICE_PREFIX);
|
if (invoiceMapper.selectByNo(no) != null) {
|
throw new IllegalArgumentException("开票编号已存在:" + no
|
+ "。请修改模板中的开票编号,或先在系统中删除该编号的开票后重新导入");
|
}
|
|
// 5. 去重:同一合同下,发票号码(留空时按开票金额、发票抬头、开票日期)相同的开票不可重复导入
|
// 开票日期留空时视作不比较该维度,故按“是否填写”传 null,不能用 parseImportDateTime 的 now() 兜底值
|
LocalDateTime invoiceTime = parseImportDateTime(row.getInvoiceTime());
|
CrmInvoiceDO duplicate = findDuplicateInvoice(contract.getId(), row,
|
StrUtil.isNotBlank(row.getInvoiceTime()) ? invoiceTime : null);
|
if (duplicate != null) {
|
throw duplicateExistingException(buildInvoiceKeyDesc(row), duplicate.getNo());
|
}
|
|
// 6. 保存开票
|
CrmInvoiceDO invoice = new CrmInvoiceDO().setNo(no).setCustomerId(contract.getCustomerId())
|
.setContractId(contract.getId()).setOwnerUserId(getLoginUserId()).setInvoiceType(invoiceType)
|
.setInvoiceNo(row.getInvoiceNo()).setInvoiceTitle(row.getInvoiceTitle()).setPrice(row.getPrice())
|
.setInvoiceTime(invoiceTime).setRemark(row.getRemark())
|
.setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus());
|
invoiceMapper.insert(invoice);
|
|
// 7. 创建数据权限(否则当前用户看不到该开票)
|
permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_INVOICE.getType())
|
.setBizId(invoice.getId()).setUserId(invoice.getOwnerUserId())
|
.setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
|
return invoice.getNo();
|
}
|
|
private Integer resolveInvoiceType(String text) {
|
List<String> labelList = DictFrameworkUtils.getDictDataLabelList(CRM_INVOICE_TYPE);
|
if (StrUtil.isBlank(text)) {
|
throw new IllegalArgumentException("发票类型不能为空:请填写模板中该行的「发票类型」列,可选值:"
|
+ String.join("、", labelList));
|
}
|
String value = text.trim();
|
if (value.matches("\\d+") && DictFrameworkUtils.getDictDataValueList(CRM_INVOICE_TYPE).contains(value)) {
|
return Integer.valueOf(value);
|
}
|
String parsedValue = DictFrameworkUtils.parseDictDataValue(CRM_INVOICE_TYPE, value);
|
if (parsedValue == null) {
|
throw new IllegalArgumentException("发票类型不存在:" + value + "。模板中「发票类型」请填写以下之一:"
|
+ String.join("、", labelList));
|
}
|
return Integer.valueOf(parsedValue);
|
}
|
|
private LocalDateTime parseImportDateTime(String text) {
|
if (StrUtil.isBlank(text)) {
|
return LocalDateTime.now();
|
}
|
String value = text.trim();
|
for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
|
try {
|
return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
|
} catch (Exception ignored) {
|
// 继续尝试下一个格式
|
}
|
}
|
for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
|
try {
|
return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
|
} catch (Exception ignored) {
|
// 继续尝试下一个格式
|
}
|
}
|
throw new IllegalArgumentException("开票日期格式不正确:" + text + ",示例 2026-01-01 10:00:00");
|
}
|
|
private String resolveImportErrorMessage(Exception ex) {
|
if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
|
return serviceException.getMessage();
|
}
|
return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
|
}
|
|
/**
|
* 导入去重键:发票号码填写时以 合同 + 发票号码 为准;留空时比较 合同 + 开票金额 + 发票抬头 + 开票日期
|
*/
|
private String buildInvoiceImportKey(CrmInvoiceImportExcelVO row) {
|
if (StrUtil.isNotBlank(row.getInvoiceNo())) {
|
return "no|" + StrUtil.trimToEmpty(row.getContractNo()) + "|" + row.getInvoiceNo().trim();
|
}
|
return "amt|" + StrUtil.trimToEmpty(row.getContractNo()) + "|"
|
+ (row.getPrice() == null ? "" : row.getPrice().stripTrailingZeros().toPlainString()) + "|"
|
+ StrUtil.trimToEmpty(row.getInvoiceTitle()) + "|" + StrUtil.trimToEmpty(row.getInvoiceTime());
|
}
|
|
private String buildInvoiceKeyDesc(CrmInvoiceImportExcelVO row) {
|
if (StrUtil.isNotBlank(row.getInvoiceNo())) {
|
return "合同编号[" + StrUtil.trimToEmpty(row.getContractNo()) + "]、发票号码[" + row.getInvoiceNo().trim() + "]";
|
}
|
return "合同编号[" + StrUtil.trimToEmpty(row.getContractNo()) + "]、开票金额["
|
+ (row.getPrice() == null ? "未填写" : row.getPrice().stripTrailingZeros().toPlainString() + " 元")
|
+ "]、发票抬头[" + StrUtil.blankToDefault(StrUtil.trimToNull(row.getInvoiceTitle()), "未填写") + "]"
|
+ (StrUtil.isBlank(row.getInvoiceTime()) ? "" : "、开票日期[" + row.getInvoiceTime().trim() + "]");
|
}
|
|
/**
|
* 查询同合同下业务数据完全相同的开票
|
*/
|
private CrmInvoiceDO findDuplicateInvoice(Long contractId, CrmInvoiceImportExcelVO row, LocalDateTime invoiceTime) {
|
for (CrmInvoiceDO old : invoiceMapper.selectListByContractId(contractId)) {
|
if (StrUtil.isNotBlank(row.getInvoiceNo())) {
|
if (StrUtil.equals(StrUtil.trimToEmpty(old.getInvoiceNo()), row.getInvoiceNo().trim())) {
|
return old;
|
}
|
continue;
|
}
|
// 发票号码留空时,比较开票金额、发票抬头;开票日期留空则不比较该维度
|
if (old.getPrice().compareTo(row.getPrice()) != 0
|
|| !StrUtil.equals(StrUtil.trimToEmpty(old.getInvoiceTitle()),
|
StrUtil.trimToEmpty(row.getInvoiceTitle()))) {
|
continue;
|
}
|
if (invoiceTime != null && !invoiceTime.equals(old.getInvoiceTime())) {
|
continue;
|
}
|
return old;
|
}
|
return null;
|
}
|
|
private IllegalArgumentException duplicateRowException(int firstRowNo, String keyDesc) {
|
return new IllegalArgumentException("检测到重复导入:本次导入的第 " + firstRowNo
|
+ " 行与当前行的数据完全相同(" + keyDesc + ")。请删除模板中重复的行后重新导入");
|
}
|
|
private IllegalArgumentException duplicateExistingException(String keyDesc, String existingNo) {
|
return new IllegalArgumentException("检测到重复导入:" + keyDesc + " 与已有开票单「" + existingNo
|
+ "」完全相同(可能是本次导入前面的行,也可能是系统中已有单据)。请勿重复导入;如确需重新导入,请先在「营销管理-开票管理」中删除该开票单后重试");
|
}
|
|
private void validateInvoicePriceExceedsLimit(CrmInvoiceSaveReqVO reqVO) {
|
// 1. 计算剩余可开票金额,不包括 reqVO 自身(审批中也占用额度,防止累计开票超过合同金额)
|
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
|
BigDecimal notInvoicePrice = getRemainingInvoicePrice(contract, reqVO.getId());
|
// 2. 校验金额是否超过
|
if (reqVO.getPrice().compareTo(notInvoicePrice) > 0) {
|
throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, notInvoicePrice);
|
}
|
}
|
|
/**
|
* 计算合同剩余可开票金额(审批中、审核通过的开票占用额度)
|
*
|
* @param contract 合同
|
* @param excludeInvoiceId 需排除的开票编号,可为空
|
*/
|
private BigDecimal getRemainingInvoicePrice(CrmContractDO contract, Long excludeInvoiceId) {
|
List<CrmInvoiceDO> invoices = invoiceMapper.selectListByContractIdAndStatus(contract.getId(),
|
List.of(CrmAuditStatusEnum.APPROVE.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus()));
|
if (excludeInvoiceId != null) {
|
invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), excludeInvoiceId));
|
}
|
return contract.getTotalPrice().subtract(
|
CollectionUtils.getSumValue(invoices, CrmInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
|
}
|
|
private void validateRelationDataExists(CrmInvoiceSaveReqVO reqVO) {
|
if (reqVO.getOwnerUserId() != null) {
|
adminUserApi.validateUser(reqVO.getOwnerUserId()); // 校验负责人存在
|
}
|
if (reqVO.getContractId() != null) {
|
CrmContractDO contract = contractService.validateContract(reqVO.getContractId());
|
if (ObjectUtil.notEqual(contract.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
|
throw exception(INVOICE_CREATE_FAIL_CONTRACT_NOT_APPROVE);
|
}
|
reqVO.setCustomerId(contract.getCustomerId()); // 设置客户编号
|
}
|
}
|
|
/**
|
* 校验销售订单、销售出库单,并回填出库单号
|
*
|
* 校验顺序:销售订单存在 -> 已审核 -> 客户/合同一致 -> 订单剩余可开票;
|
* 再校验出库单存在 -> 已完成 -> 属于该订单 -> 客户一致 -> 出库单剩余可开票。
|
*/
|
private void validateSaleOrderAndOut(CrmInvoiceSaveReqVO reqVO) {
|
if (reqVO.getSaleOrderId() == null) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_REQUIRED);
|
}
|
// 1. 销售订单
|
ErpSaleOrderRespDTO saleOrder = saleOrderApi.getSaleOrder(reqVO.getSaleOrderId()).getCheckedData();
|
if (saleOrder == null) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_NOT_EXISTS, reqVO.getSaleOrderId());
|
}
|
if (ObjectUtil.notEqual(saleOrder.getStatus(), ERP_SALE_ORDER_STATUS_APPROVE)) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_NOT_APPROVE, saleOrder.getNo(),
|
erpAuditStatusName(saleOrder.getStatus()));
|
}
|
if (ObjectUtil.notEqual(saleOrder.getCustomerId(), reqVO.getCustomerId())
|
|| (saleOrder.getContractId() != null
|
&& ObjectUtil.notEqual(saleOrder.getContractId(), reqVO.getContractId()))) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_ORDER_MISMATCH, saleOrder.getNo(),
|
saleOrder.getCustomerId(), saleOrder.getContractId(), reqVO.getCustomerId(), reqVO.getContractId());
|
}
|
// 1.1 订单剩余可开票金额 = 订单金额 - 该订单已占用开票金额(审批中 + 审批通过)
|
BigDecimal orderTotalPrice = zeroIfNull(saleOrder.getTotalPrice());
|
BigDecimal orderInvoicedPrice = sumInvoicePrice(invoiceMapper.selectListBySaleOrderIdAndStatus(
|
reqVO.getSaleOrderId(), INVOICE_OCCUPY_STATUSES), reqVO.getId());
|
BigDecimal orderRemainPrice = orderTotalPrice.subtract(orderInvoicedPrice);
|
if (reqVO.getPrice() != null && reqVO.getPrice().compareTo(orderRemainPrice) > 0) {
|
throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_SALE_ORDER, saleOrder.getNo(),
|
formatPrice(orderRemainPrice), formatPrice(orderTotalPrice), formatPrice(orderInvoicedPrice));
|
}
|
|
// 2. 开票申请必须绑定已完成的销售出库单
|
if (reqVO.getSaleOutId() == null) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_OUT_REQUIRED);
|
}
|
MesWmProductSalesRespDTO saleOut = productSalesApi.getProductSales(reqVO.getSaleOutId());
|
if (saleOut == null) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_OUT_NOT_EXISTS, reqVO.getSaleOutId());
|
}
|
if (ObjectUtil.notEqual(saleOut.getStatus(), MES_PRODUCT_SALES_STATUS_FINISHED)) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_OUT_NOT_FINISHED, saleOut.getCode(),
|
productSalesStatusName(saleOut.getStatus()));
|
}
|
if (saleOut.getSaleOrderId() != null && ObjectUtil.notEqual(saleOut.getSaleOrderId(), reqVO.getSaleOrderId())) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_OUT_MISMATCH_ORDER, saleOut.getCode(),
|
saleOrder.getNo(), saleOut.getSaleOrderId());
|
}
|
if (saleOut.getClientId() != null && ObjectUtil.notEqual(saleOut.getClientId(), reqVO.getCustomerId())) {
|
throw exception(INVOICE_CREATE_FAIL_SALE_OUT_MISMATCH_CUSTOMER, saleOut.getCode(),
|
saleOut.getClientId(), reqVO.getCustomerId());
|
}
|
// 2.1 出库单剩余可开票金额 = 出库金额 - 该出库单已占用开票金额(审批中 + 审批通过)
|
BigDecimal saleOutTotalPrice = zeroIfNull(saleOut.getTotalPrice());
|
BigDecimal saleOutInvoicedPrice = sumInvoicePrice(invoiceMapper.selectListBySaleOutIdAndStatus(
|
reqVO.getSaleOutId(), INVOICE_OCCUPY_STATUSES), reqVO.getId());
|
BigDecimal saleOutRemainPrice = saleOutTotalPrice.subtract(saleOutInvoicedPrice);
|
if (reqVO.getPrice() != null && reqVO.getPrice().compareTo(saleOutRemainPrice) > 0) {
|
throw exception(INVOICE_CREATE_FAIL_PRICE_EXCEEDS_SALE_OUT, saleOut.getCode(),
|
formatPrice(saleOutRemainPrice), formatPrice(saleOutTotalPrice), formatPrice(saleOutInvoicedPrice));
|
}
|
reqVO.setSaleOutCode(saleOut.getCode());
|
}
|
|
private BigDecimal sumInvoicePrice(List<CrmInvoiceDO> invoices, Long excludeInvoiceId) {
|
if (CollUtil.isEmpty(invoices)) {
|
return BigDecimal.ZERO;
|
}
|
return invoices.stream()
|
.filter(invoice -> excludeInvoiceId == null || ObjectUtil.notEqual(invoice.getId(), excludeInvoiceId))
|
.map(invoice -> zeroIfNull(invoice.getPrice()))
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
}
|
|
/**
|
* 根据发票号码是否填写,设置发票录入状态
|
*/
|
private void applyEntryStatus(CrmInvoiceDO invoice, String invoiceNo, CrmInvoiceDO oldInvoice) {
|
if (StrUtil.isBlank(invoiceNo)) {
|
invoice.setEntryStatus(0);
|
return;
|
}
|
invoice.setEntryStatus(10);
|
boolean changed = oldInvoice == null || !StrUtil.equals(invoiceNo, oldInvoice.getInvoiceNo())
|
|| !Integer.valueOf(10).equals(oldInvoice.getEntryStatus());
|
if (changed) {
|
invoice.setEntryUserId(getLoginUserId()).setEntryTime(LocalDateTime.now());
|
}
|
}
|
|
/**
|
* 同步开票单的收款节点到回款计划(crm_receivable_plan)
|
*
|
* 传 null 表示本次不修改节点;传空列表表示清空节点。
|
*/
|
private void syncPaymentNodes(CrmInvoiceDO invoice, List<CrmInvoiceSaveReqVO.PaymentNode> nodes) {
|
if (nodes == null) {
|
return;
|
}
|
validatePaymentNodes(nodes);
|
// 已产生回款的节点不允许被覆盖
|
for (CrmReceivablePlanDO oldPlan : receivablePlanMapper.selectListByInvoiceId(invoice.getId())) {
|
if (oldPlan.getReceivableId() != null || zeroIfNull(oldPlan.getReceiptedPrice()).compareTo(BigDecimal.ZERO) > 0) {
|
throw exception(INVOICE_PAYMENT_NODE_ALREADY_RECEIPTED,
|
CrmPaymentStageEnum.getNameByStage(oldPlan.getStage()),
|
formatPrice(zeroIfNull(oldPlan.getReceiptedPrice())));
|
}
|
}
|
deletePaymentNodesByInvoiceId(invoice.getId());
|
|
Long ownerUserId = invoice.getOwnerUserId() != null ? invoice.getOwnerUserId() : getLoginUserId();
|
for (CrmInvoiceSaveReqVO.PaymentNode node : nodes) {
|
CrmReceivablePlanDO maxPeriodPlan = receivablePlanMapper.selectMaxPeriodByContractId(invoice.getContractId());
|
int period = maxPeriodPlan == null ? 1 : maxPeriodPlan.getPeriod() + 1;
|
int remindDays = 3;
|
CrmReceivablePlanDO plan = CrmReceivablePlanDO.builder()
|
.period(period).customerId(invoice.getCustomerId()).contractId(invoice.getContractId())
|
.ownerUserId(ownerUserId).returnTime(node.getReturnTime())
|
.remindDays(remindDays).remindTime(node.getReturnTime().minusDays(remindDays))
|
.price(node.getAmount()).stage(node.getStage()).invoiceId(invoice.getId())
|
.saleOrderId(invoice.getSaleOrderId())
|
.receiptedPrice(BigDecimal.ZERO).status(0).remark(node.getRemark())
|
.build();
|
receivablePlanMapper.insert(plan);
|
permissionService.createPermission(new CrmPermissionCreateReqBO()
|
.setBizType(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType()).setBizId(plan.getId())
|
.setUserId(ownerUserId).setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
|
}
|
}
|
|
private void validatePaymentNodes(List<CrmInvoiceSaveReqVO.PaymentNode> nodes) {
|
if (CollUtil.isEmpty(nodes)) {
|
return;
|
}
|
Set<Integer> stages = new HashSet<>();
|
for (int i = 0; i < nodes.size(); i++) {
|
CrmInvoiceSaveReqVO.PaymentNode node = nodes.get(i);
|
int row = i + 1;
|
CrmPaymentStageEnum stageEnum = node == null ? null : CrmPaymentStageEnum.valueOf(node.getStage());
|
if (stageEnum == null) {
|
throw exception(INVOICE_PAYMENT_NODE_STAGE_INVALID, row, node == null ? "未填写" : node.getStage());
|
}
|
if (!stages.add(stageEnum.getStage())) {
|
throw exception(INVOICE_PAYMENT_NODE_STAGE_DUPLICATE, stageEnum.getName());
|
}
|
if (node.getAmount() == null || node.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
throw exception(INVOICE_PAYMENT_NODE_INVALID, row, stageEnum.getName(),
|
node.getAmount() == null ? "未填写" : formatPrice(node.getAmount()));
|
}
|
if (node.getReturnTime() == null) {
|
throw exception(INVOICE_PAYMENT_NODE_TIME_REQUIRED, stageEnum.getName());
|
}
|
}
|
}
|
|
private void deletePaymentNodesByInvoiceId(Long invoiceId) {
|
for (CrmReceivablePlanDO plan : receivablePlanMapper.selectListByInvoiceId(invoiceId)) {
|
receivablePlanMapper.deleteById(plan.getId());
|
permissionService.deletePermission(CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), plan.getId());
|
}
|
}
|
|
private String erpAuditStatusName(Integer status) {
|
if (Objects.equals(status, ERP_SALE_ORDER_STATUS_APPROVE)) {
|
return "已审核";
|
}
|
if (status == null) {
|
return "未知状态";
|
}
|
return switch (status) {
|
case 0 -> "未提交";
|
case 10 -> "未审核";
|
case 15 -> "审批中";
|
case 30 -> "已作废";
|
case 35 -> "已驳回";
|
default -> "状态值 " + status;
|
};
|
}
|
|
private String productSalesStatusName(Integer status) {
|
if (status == null) {
|
return "未知状态";
|
}
|
return switch (status) {
|
case 0 -> "草稿";
|
case 1 -> "待检测";
|
case 2 -> "待拣货";
|
case 3 -> "待执行出库";
|
case 4 -> "已完成";
|
case 5 -> "已取消";
|
case 10 -> "待填写运单";
|
default -> "状态值 " + status;
|
};
|
}
|
|
private static BigDecimal zeroIfNull(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
private static String formatPrice(BigDecimal value) {
|
return (value == null ? BigDecimal.ZERO : value).stripTrailingZeros().toPlainString();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
|
success = CRM_INVOICE_UPDATE_SUCCESS)
|
@CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.WRITE)
|
public void updateInvoice(CrmInvoiceSaveReqVO updateReqVO) {
|
Assert.notNull(updateReqVO.getId(), "开票编号不能为空");
|
updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null); // 不允许修改的字段
|
// 1.1 校验存在
|
CrmInvoiceDO oldInvoice = validateInvoiceExists(updateReqVO.getId());
|
updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()).setCustomerId(oldInvoice.getCustomerId())
|
.setContractId(oldInvoice.getContractId()); // 设置已存在的值
|
// 1.2 免审批时不限制编辑状态;正常审批仅草稿、审批中可以编辑。
|
if (!bpmProcessInstanceApi.isGlobalAutoApprove()
|
&& !ObjectUtils.equalsAny(oldInvoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
|
CrmAuditStatusEnum.PROCESS.getStatus())) {
|
throw exception(INVOICE_UPDATE_FAIL_EDITING_PROHIBITED,
|
StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(oldInvoice.getAuditStatus()), "未知状态"));
|
}
|
|
// 1.3 校验销售订单、销售出库单,并回填出库单号
|
validateSaleOrderAndOut(updateReqVO);
|
// 1.4 校验可开票金额超过上限
|
validateInvoicePriceExceedsLimit(updateReqVO);
|
|
// 2. 更新开票
|
CrmInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, CrmInvoiceDO.class);
|
applyEntryStatus(updateObj, updateReqVO.getInvoiceNo(), oldInvoice);
|
invoiceMapper.updateById(updateObj);
|
// 发票号码被清空时,一并清空录入人、录入时间(updateById 会忽略 null 字段,需显式清空)
|
if (StrUtil.isBlank(updateReqVO.getInvoiceNo())) {
|
invoiceMapper.clearEntryInfo(updateReqVO.getId());
|
}
|
|
// 3. 增量更新发票附件
|
storageAttachmentApi.updateAttachments("file", INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
|
|
// 3.1 同步收款节点(仅前端传了 paymentNodes 才覆盖,避免不传时误删)
|
syncPaymentNodes(oldInvoice, updateReqVO.getPaymentNodes());
|
|
// 4. 记录操作日志上下文
|
updateReqVO.setOwnerUserId(oldInvoice.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
|
LogRecordContext.putVariable("invoice", oldInvoice);
|
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldInvoice, CrmInvoiceSaveReqVO.class));
|
}
|
|
@Override
|
public void updateInvoiceAuditStatus(Long id, Integer bpmResult) {
|
// 1.1 校验存在
|
CrmInvoiceDO invoice = validateInvoiceExists(id);
|
// 1.2 只有审批中,可以更新审批结果
|
if (ObjUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) {
|
log.error("[updateInvoiceAuditStatus][invoice({}) 不处于审批中,无法更新审批结果({})]",
|
invoice.getId(), bpmResult);
|
throw exception(INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
|
}
|
|
// 2. 更新开票审批状态
|
Integer auditStatus = convertBpmResultToAuditStatus(bpmResult);
|
invoiceMapper.updateById(new CrmInvoiceDO().setId(id).setAuditStatus(auditStatus));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_DELETE_SUB_TYPE, bizNo = "{{#id}}",
|
success = CRM_INVOICE_DELETE_SUCCESS)
|
@CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#id", level = CrmPermissionLevelEnum.OWNER)
|
public void deleteInvoice(Long id) {
|
// 1.1 校验存在
|
CrmInvoiceDO invoice = validateInvoiceExists(id);
|
// 1.2 审批通过时,不允许删除
|
if (ObjUtil.equal(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
|
throw exception(INVOICE_DELETE_FAIL_IS_APPROVE);
|
}
|
|
// 2.1 删除发票附件
|
storageAttachmentApi.deleteAttachmentsByRecord(INVOICE_RECORD_TYPE, id);
|
// 2.2 删除开票
|
invoiceMapper.deleteById(id);
|
// 2.3 删除数据权限
|
permissionService.deletePermission(CrmBizTypeEnum.CRM_INVOICE.getType(), id);
|
// 2.4 删除开票生成的收款节点(审批通过的开票不允许删除,故此处节点必然未产生回款)
|
deletePaymentNodesByInvoiceId(id);
|
|
// 3. 记录操作日志上下文
|
LogRecordContext.putVariable("invoice", invoice);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
|
success = CRM_INVOICE_SUBMIT_SUCCESS)
|
public void submitInvoice(Long id, Long userId) {
|
// 1. 校验开票是否在审批
|
CrmInvoiceDO invoice = validateInvoiceExists(id);
|
if (ObjUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus())) {
|
throw exception(INVOICE_SUBMIT_FAIL_NOT_DRAFT);
|
}
|
|
// 1.1 重新校验可开票金额,防止累计开票超过合同金额(草稿期后可能有其他单进入审批中)
|
validateInvoicePriceExceedsLimit(new CrmInvoiceSaveReqVO()
|
.setId(invoice.getId()).setContractId(invoice.getContractId()).setPrice(invoice.getPrice()));
|
|
// 2. 创建开票审批流程实例
|
// 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型。
|
if (bpmProcessInstanceApi.isGlobalAutoApprove()) {
|
invoiceMapper.updateById(new CrmInvoiceDO().setId(id).setProcessInstanceId("")
|
.setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()));
|
updateInvoiceAuditStatus(id, cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus());
|
LogRecordContext.putVariable("invoiceNo", invoice.getNo());
|
return;
|
}
|
|
String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
|
.setProcessDefinitionKey(BPM_PROCESS_DEFINITION_KEY).setBusinessKey(String.valueOf(id)));
|
|
// 3. 更新开票工作流编号
|
invoiceMapper.updateById(new CrmInvoiceDO().setId(id).setProcessInstanceId(processInstanceId)
|
.setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()));
|
|
// 4. 记录日志
|
LogRecordContext.putVariable("invoiceNo", invoice.getNo());
|
}
|
|
private CrmInvoiceDO validateInvoiceExists(Long id) {
|
CrmInvoiceDO invoice = invoiceMapper.selectById(id);
|
if (invoice == null) {
|
throw exception(INVOICE_NOT_EXISTS);
|
}
|
return invoice;
|
}
|
|
@Override
|
@CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#id", level = CrmPermissionLevelEnum.READ)
|
public CrmInvoiceDO getInvoice(Long id) {
|
return invoiceMapper.selectById(id);
|
}
|
|
@Override
|
public List<CrmInvoiceDO> getInvoiceList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return ListUtil.empty();
|
}
|
return invoiceMapper.selectByIds(ids);
|
}
|
|
@Override
|
public PageResult<CrmInvoiceDO> getInvoicePage(CrmInvoicePageReqVO pageReqVO, Long userId) {
|
return invoiceMapper.selectPage(pageReqVO, userId);
|
}
|
|
@Override
|
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#pageReqVO.customerId", level = CrmPermissionLevelEnum.READ)
|
public PageResult<CrmInvoiceDO> getInvoicePageByCustomerId(CrmInvoicePageReqVO pageReqVO) {
|
return invoiceMapper.selectPageByCustomerId(pageReqVO);
|
}
|
|
@Override
|
public Long getAuditInvoiceCount(Long userId) {
|
return invoiceMapper.selectCountByAudit(userId);
|
}
|
|
@Override
|
public List<CrmInvoiceDO> getInvoiceListByContractIdAndStatus(Long contractId, Collection<Integer> auditStatuses) {
|
return invoiceMapper.selectListByContractIdAndStatus(contractId, auditStatuses);
|
}
|
|
@Override
|
public Map<Long, BigDecimal> getInvoicePriceMapByContractId(Collection<Long> contractIds) {
|
return invoiceMapper.selectInvoicePriceMapByContractId(contractIds);
|
}
|
|
@Override
|
public Long getInvoiceCountByContractId(Long contractId) {
|
return invoiceMapper.selectCountByContractId(contractId);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
@LogRecord(type = CRM_INVOICE_TYPE, subType = CRM_INVOICE_ENTRY_SUB_TYPE, bizNo = "{{#reqVO.id}}",
|
success = CRM_INVOICE_ENTRY_SUCCESS)
|
@CrmPermission(bizType = CrmBizTypeEnum.CRM_INVOICE, bizId = "#reqVO.id", level = CrmPermissionLevelEnum.WRITE)
|
public void entryInvoice(CrmInvoiceEntryReqVO reqVO) {
|
// 1.1 校验存在
|
CrmInvoiceDO invoice = validateInvoiceExists(reqVO.getId());
|
// 1.2 只有审批通过的开票申请才能录入发票
|
if (ObjectUtil.notEqual(invoice.getAuditStatus(), CrmAuditStatusEnum.APPROVE.getStatus())) {
|
throw exception(INVOICE_ENTRY_FAIL_NOT_APPROVE, invoice.getNo(),
|
StrUtil.blankToDefault(CrmAuditStatusEnum.getNameByStatus(invoice.getAuditStatus()), "未知状态"));
|
}
|
// 1.3 发票号码不可与其他开票单重复
|
String invoiceNo = reqVO.getInvoiceNo().trim();
|
CrmInvoiceDO invoiceNoExists = invoiceMapper.selectByInvoiceNo(invoiceNo);
|
if (invoiceNoExists != null && ObjectUtil.notEqual(invoiceNoExists.getId(), invoice.getId())) {
|
throw exception(INVOICE_ENTRY_FAIL_INVOICE_NO_EXISTS, invoiceNo, invoiceNoExists.getNo());
|
}
|
|
// 2. 录入发票信息,状态置为已录入
|
invoiceMapper.updateById(new CrmInvoiceDO().setId(invoice.getId())
|
.setInvoiceNo(invoiceNo).setInvoiceTitle(reqVO.getInvoiceTitle())
|
.setInvoiceTime(reqVO.getInvoiceTime()).setRemark(reqVO.getRemark())
|
.setEntryStatus(10).setEntryUserId(getLoginUserId()).setEntryTime(LocalDateTime.now()));
|
// 3. 更新发票附件
|
if (reqVO.getBlobIds() != null) {
|
storageAttachmentApi.updateAttachments("file", INVOICE_RECORD_TYPE, invoice.getId(), reqVO.getBlobIds());
|
}
|
// 4. 同步收款节点
|
syncPaymentNodes(invoice, reqVO.getPaymentNodes());
|
|
// 5. 记录操作日志上下文
|
LogRecordContext.putVariable("invoiceNo", invoice.getNo());
|
LogRecordContext.putVariable("entryInvoiceNo", invoiceNo);
|
}
|
|
@Override
|
public List<CrmInvoiceSaleOutRespVO> getSaleOutListForInvoice(Long saleOrderId, Long excludeInvoiceId) {
|
if (saleOrderId == null) {
|
return ListUtil.empty();
|
}
|
List<MesWmProductSalesRespDTO> saleOutList = productSalesApi.getProductSalesListBySaleOrderId(saleOrderId);
|
if (CollUtil.isEmpty(saleOutList)) {
|
return ListUtil.empty();
|
}
|
List<CrmInvoiceSaleOutRespVO> result = new ArrayList<>();
|
for (MesWmProductSalesRespDTO saleOut : saleOutList) {
|
// 已取消的出库单不可开票
|
if (ObjectUtil.equal(saleOut.getStatus(), MES_PRODUCT_SALES_STATUS_CANCELED)) {
|
continue;
|
}
|
BigDecimal totalPrice = zeroIfNull(saleOut.getTotalPrice());
|
BigDecimal invoicedPrice = sumInvoicePrice(invoiceMapper.selectListBySaleOutIdAndStatus(
|
saleOut.getId(), INVOICE_OCCUPY_STATUSES), excludeInvoiceId);
|
CrmInvoiceSaleOutRespVO vo = new CrmInvoiceSaleOutRespVO();
|
vo.setId(saleOut.getId());
|
vo.setCode(saleOut.getCode());
|
vo.setSaleOrderId(saleOut.getSaleOrderId());
|
vo.setSalesOrderCode(saleOut.getSalesOrderCode());
|
vo.setSalesDate(saleOut.getSalesDate());
|
vo.setStatus(saleOut.getStatus());
|
vo.setStatusName(productSalesStatusName(saleOut.getStatus()));
|
vo.setTotalPrice(totalPrice);
|
vo.setInvoicedPrice(invoicedPrice);
|
vo.setRemainInvoicePrice(totalPrice.subtract(invoicedPrice));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
@Override
|
public List<CrmReceivablePlanDO> getPaymentNodeListByInvoiceId(Long invoiceId) {
|
return receivablePlanMapper.selectListByInvoiceId(invoiceId);
|
}
|
|
}
|