package cn.iocoder.yudao.module.erp.service.purchase;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceItemReceiptRespVO;
|
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
|
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO;
|
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
|
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
|
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
|
import cn.iocoder.yudao.module.mes.api.itemreceipt.MesWmItemReceiptApi;
|
import cn.iocoder.yudao.module.mes.api.itemreceipt.dto.MesWmItemReceiptLineRespDTO;
|
import cn.iocoder.yudao.module.mes.api.itemreceipt.dto.MesWmItemReceiptRespDTO;
|
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
|
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceMapper;
|
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
|
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
|
import jakarta.annotation.Resource;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.stream.Collectors;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
|
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
|
|
/**
|
* ERP 采购来票 Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@Validated
|
public class ErpPurchaseInvoiceServiceImpl implements ErpPurchaseInvoiceService {
|
|
/**
|
* 来票附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#ERP_PURCHASE_INVOICE}
|
*/
|
public static final String PURCHASE_INVOICE_RECORD_TYPE = "erp_purchase_invoice";
|
|
@Resource
|
private ErpPurchaseInvoiceMapper invoiceMapper;
|
|
@Resource
|
private ErpFinancePaymentMapper financePaymentMapper;
|
|
@Resource
|
private ErpNoRedisDAO noRedisDAO;
|
|
@Resource
|
private ErpPurchaseOrderService purchaseOrderService;
|
|
@Resource
|
private MesWmItemReceiptApi mesWmItemReceiptApi;
|
|
@Resource
|
private StorageAttachmentApi storageAttachmentApi;
|
|
/**
|
* MES 入库单已完成状态,见 MesWmItemReceiptStatusEnum.FINISHED
|
*/
|
private static final Integer ITEM_RECEIPT_STATUS_FINISHED = 4;
|
|
/**
|
* MES 入库单已取消状态,见 MesWmItemReceiptStatusEnum.CANCELED
|
*/
|
private static final Integer ITEM_RECEIPT_STATUS_CANCELED = 5;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createPurchaseInvoice(ErpPurchaseInvoiceSaveReqVO createReqVO) {
|
// 1.1 校验关联数据存在
|
validateRelationDataExists(createReqVO);
|
// 1.2 校验可开票金额超过上限
|
validatePurchaseInvoicePriceExceedsLimit(createReqVO);
|
// 1.3 生成来票编号
|
String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_INVOICE_NO_PREFIX);
|
if (invoiceMapper.selectByNo(no) != null) {
|
throw exception(PURCHASE_INVOICE_NO_EXISTS);
|
}
|
|
// 2.1 插入来票(创建后即为有效状态,无需审核)
|
ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no);
|
applyItemReceiptCode(invoice);
|
invoiceMapper.insert(invoice);
|
|
// 2.2 绑定发票附件
|
if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
|
storageAttachmentApi.bindAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds());
|
}
|
return invoice.getId();
|
}
|
|
private void validateRelationDataExists(ErpPurchaseInvoiceSaveReqVO reqVO) {
|
// 校验采购订单,必须审核通过
|
ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(reqVO.getPurchaseOrderId());
|
// 校验供应商与采购订单一致(防御脏数据:来票必须属于该采购订单的供应商)
|
if (reqVO.getSupplierId() != null && ObjectUtil.notEqual(purchaseOrder.getSupplierId(), reqVO.getSupplierId())) {
|
throw exception(PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH);
|
}
|
reqVO.setSupplierId(purchaseOrder.getSupplierId()); // 供应商以采购订单为准
|
// 校验入库单,并回填入库单号快照
|
validateItemReceipt(reqVO, purchaseOrder);
|
}
|
|
/**
|
* 校验来票绑定的入库单:必须存在、已完成入库、属于当前采购订单和供应商
|
*/
|
private void validateItemReceipt(ErpPurchaseInvoiceSaveReqVO reqVO, ErpPurchaseOrderDO purchaseOrder) {
|
if (reqVO.getItemReceiptId() == null) {
|
return; // 存量/未绑定时允许为空
|
}
|
MesWmItemReceiptRespDTO receipt = mesWmItemReceiptApi.getItemReceipt(reqVO.getItemReceiptId());
|
if (receipt == null) {
|
throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_NOT_EXISTS.getCode(),
|
"来票关联的入库单不存在:入库单 ID=" + reqVO.getItemReceiptId()
|
+ "。请重新选择该采购订单下的入库单;若列表中没有可选项,请先在 MES 完成入库。");
|
}
|
if (!ITEM_RECEIPT_STATUS_FINISHED.equals(receipt.getStatus())) {
|
throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_NOT_FINISHED.getCode(),
|
"来票关联的入库单[" + receipt.getCode() + "]当前状态为「" + itemReceiptStatusName(receipt.getStatus())
|
+ "」,只有「已完成」的入库单才能来票。请先在 MES 完成入库后再选择该入库单。");
|
}
|
if (ObjectUtil.notEqual(receipt.getPurchaseOrderId(), reqVO.getPurchaseOrderId())) {
|
throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_ORDER_MISMATCH.getCode(),
|
"所选入库单[" + receipt.getCode() + "]属于采购订单[" + StrUtil.blankToDefault(receipt.getPurchaseOrderCode(), "ID=" + receipt.getPurchaseOrderId())
|
+ "],与当前选择的采购订单[" + purchaseOrder.getNo() + "]不一致。请重新选择该采购订单下的入库单,或先核对采购订单号。");
|
}
|
if (ObjectUtil.notEqual(receipt.getVendorId(), purchaseOrder.getSupplierId())) {
|
throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_SUPPLIER_MISMATCH.getCode(),
|
"所选入库单[" + receipt.getCode() + "]的供应商(ID=" + receipt.getVendorId()
|
+ ")与采购订单[" + purchaseOrder.getNo() + "]的供应商(ID=" + purchaseOrder.getSupplierId()
|
+ ")不一致。请核对入库单来源,或改选该采购订单下的入库单。");
|
}
|
}
|
|
/**
|
* 保存入库单号快照,列表与导出直接展示,避免联表
|
*/
|
private void applyItemReceiptCode(ErpPurchaseInvoiceDO invoice) {
|
if (invoice.getItemReceiptId() == null) {
|
invoice.setItemReceiptCode(null);
|
return;
|
}
|
MesWmItemReceiptRespDTO receipt = mesWmItemReceiptApi.getItemReceipt(invoice.getItemReceiptId());
|
invoice.setItemReceiptCode(receipt != null ? receipt.getCode() : null);
|
}
|
|
private String itemReceiptStatusName(Integer status) {
|
if (status == null) {
|
return "未知";
|
}
|
return switch (status) {
|
case 0 -> "草稿";
|
case 2 -> "待上架";
|
case 3 -> "待执行入库";
|
case 4 -> "已完成";
|
case 5 -> "已取消";
|
default -> "状态" + status;
|
};
|
}
|
|
private void validatePurchaseInvoicePriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO) {
|
// 1. 计算剩余可开票金额,不包括 reqVO 自身(所有有效来票都占用额度,防止累计来票超过采购订单金额)
|
ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(reqVO.getPurchaseOrderId());
|
List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderId(reqVO.getPurchaseOrderId());
|
if (reqVO.getId() != null) {
|
invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
|
}
|
BigDecimal remainPrice = purchaseOrder.getTotalPrice().subtract(
|
getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO));
|
// 2. 校验金额是否超过采购订单可开票金额
|
if (reqVO.getPrice().compareTo(remainPrice) > 0) {
|
throw exception(PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT, remainPrice);
|
}
|
// 3. 绑定了入库单时,还要校验不超过入库单可开票金额(入库金额 - 该入库单已来票金额)
|
if (reqVO.getItemReceiptId() != null) {
|
MesWmItemReceiptRespDTO receipt = mesWmItemReceiptApi.getItemReceipt(reqVO.getItemReceiptId());
|
BigDecimal receiptTotalPrice = calcItemReceiptTotalPrice(receipt);
|
BigDecimal invoicedPrice = getItemReceiptInvoicedPrice(reqVO.getItemReceiptId(), reqVO.getId());
|
BigDecimal receiptRemainPrice = receiptTotalPrice.subtract(invoicedPrice);
|
if (reqVO.getPrice().compareTo(receiptRemainPrice) > 0) {
|
throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_RECEIPT_PRICE_EXCEEDS_LIMIT.getCode(),
|
"本次来票金额 " + reqVO.getPrice().stripTrailingZeros().toPlainString() + " 元,超过所选入库单["
|
+ (receipt != null ? receipt.getCode() : "ID=" + reqVO.getItemReceiptId()) + "]的可开票金额 "
|
+ receiptRemainPrice.stripTrailingZeros().toPlainString() + " 元(入库金额 "
|
+ receiptTotalPrice.stripTrailingZeros().toPlainString() + " 元,已来票 "
|
+ invoicedPrice.stripTrailingZeros().toPlainString()
|
+ " 元)。请调整来票金额,或改选其他入库单;如需按整单开票,请先核对入库金额。");
|
}
|
}
|
}
|
|
/**
|
* 计算入库单金额:优先取入库行金额快照,其次按行单价 × 入库数量,最后按采购订单行单价兜底
|
*/
|
private BigDecimal calcItemReceiptTotalPrice(MesWmItemReceiptRespDTO receipt) {
|
if (receipt == null || CollUtil.isEmpty(receipt.getLines())) {
|
return BigDecimal.ZERO;
|
}
|
Map<Long, BigDecimal> orderItemPriceMap = purchaseOrderService
|
.getPurchaseOrderItemListByOrderId(receipt.getPurchaseOrderId()).stream()
|
.filter(item -> item.getProductPrice() != null)
|
.collect(Collectors.toMap(ErpPurchaseOrderItemDO::getId, ErpPurchaseOrderItemDO::getProductPrice, (a, b) -> a));
|
BigDecimal total = BigDecimal.ZERO;
|
for (MesWmItemReceiptLineRespDTO line : receipt.getLines()) {
|
BigDecimal amount = line.getTotalPrice();
|
if (amount == null && line.getPrice() != null && line.getReceivedQuantity() != null) {
|
amount = line.getPrice().multiply(line.getReceivedQuantity());
|
}
|
if (amount == null && line.getPurchaseOrderItemId() != null && line.getReceivedQuantity() != null) {
|
BigDecimal orderItemPrice = orderItemPriceMap.get(line.getPurchaseOrderItemId());
|
if (orderItemPrice != null) {
|
amount = orderItemPrice.multiply(line.getReceivedQuantity());
|
}
|
}
|
if (amount != null) {
|
total = total.add(amount);
|
}
|
}
|
return total;
|
}
|
|
/**
|
* 计算某入库单已来票金额(可按来票编号排除自身)
|
*/
|
private BigDecimal getItemReceiptInvoicedPrice(Long itemReceiptId, Long excludeInvoiceId) {
|
List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByItemReceiptId(itemReceiptId);
|
if (excludeInvoiceId != null) {
|
invoices.removeIf(invoice -> Objects.equals(invoice.getId(), excludeInvoiceId));
|
}
|
return getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updatePurchaseInvoice(ErpPurchaseInvoiceSaveReqVO updateReqVO) {
|
Assert.notNull(updateReqVO.getId(), "来票编号不能为空");
|
// 1.1 校验存在
|
validatePurchaseInvoiceExists(updateReqVO.getId());
|
// 1.2 校验关联数据 + 可开票金额
|
validateRelationDataExists(updateReqVO);
|
validatePurchaseInvoicePriceExceedsLimit(updateReqVO);
|
|
// 2. 更新来票
|
ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class);
|
applyItemReceiptCode(updateObj);
|
invoiceMapper.updateById(updateObj);
|
|
// 3. 增量更新发票附件
|
storageAttachmentApi.updateAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deletePurchaseInvoice(Long id) {
|
// 1.1 校验存在
|
validatePurchaseInvoiceExists(id);
|
// 1.2 已有有效付款单关联的来票,不允许删除(保持来票与付款的关联完整性)
|
Map<Long, BigDecimal> paidMap = financePaymentMapper.selectPaymentPriceMapByInvoiceId(List.of(id));
|
if (paidMap.getOrDefault(id, BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) {
|
throw exception(PURCHASE_INVOICE_DELETE_FAIL_HAS_PAYMENT);
|
}
|
|
// 2.1 删除发票附件
|
storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id);
|
// 2.2 删除来票
|
invoiceMapper.deleteById(id);
|
}
|
|
private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) {
|
ErpPurchaseInvoiceDO invoice = invoiceMapper.selectById(id);
|
if (invoice == null) {
|
throw exception(PURCHASE_INVOICE_NOT_EXISTS);
|
}
|
return invoice;
|
}
|
|
@Override
|
public ErpPurchaseInvoiceDO getPurchaseInvoice(Long id) {
|
return invoiceMapper.selectById(id);
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return List.of();
|
}
|
return invoiceMapper.selectByIds(ids);
|
}
|
|
@Override
|
public PageResult<ErpPurchaseInvoiceDO> getPurchaseInvoicePage(ErpPurchaseInvoicePageReqVO pageReqVO) {
|
return invoiceMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId) {
|
return invoiceMapper.selectListByOrderId(orderId);
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierId(Long supplierId) {
|
return invoiceMapper.selectListBySupplierId(supplierId);
|
}
|
|
@Override
|
public Map<Long, BigDecimal> getPurchaseInvoicePriceSumMapByOrderIds(Collection<Long> orderIds) {
|
if (CollUtil.isEmpty(orderIds)) {
|
return Collections.emptyMap();
|
}
|
List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderIds(orderIds);
|
return invoices.stream().collect(Collectors.groupingBy(ErpPurchaseInvoiceDO::getPurchaseOrderId,
|
Collectors.mapping(ErpPurchaseInvoiceDO::getPrice, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceItemReceiptRespVO> getItemReceiptListForInvoice(Long purchaseOrderId, Long excludeInvoiceId) {
|
if (purchaseOrderId == null) {
|
return List.of();
|
}
|
List<MesWmItemReceiptRespDTO> receipts = mesWmItemReceiptApi.getItemReceiptListByPurchaseOrderId(purchaseOrderId);
|
return receipts.stream()
|
.filter(receipt -> !ITEM_RECEIPT_STATUS_CANCELED.equals(receipt.getStatus()))
|
.map(receipt -> {
|
BigDecimal totalPrice = calcItemReceiptTotalPrice(receipt);
|
BigDecimal invoicedPrice = getItemReceiptInvoicedPrice(receipt.getId(), excludeInvoiceId);
|
return new ErpPurchaseInvoiceItemReceiptRespVO()
|
.setId(receipt.getId())
|
.setCode(receipt.getCode())
|
.setName(receipt.getName())
|
.setReceiptDate(receipt.getReceiptDate())
|
.setStatus(receipt.getStatus())
|
.setTotalPrice(totalPrice)
|
.setInvoicedPrice(invoicedPrice)
|
.setRemainInvoicePrice(totalPrice.subtract(invoicedPrice));
|
})
|
.toList();
|
}
|
|
}
|