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.ErpPurchaseInvoiceOrderReqVO;
|
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.ErpPurchaseInvoiceOrderDO;
|
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
|
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.mysql.purchase.ErpPurchaseInvoiceOrderMapper;
|
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
|
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
|
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
|
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.ArrayList;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.stream.Collectors;
|
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
|
|
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 ErpPurchaseInvoiceOrderMapper invoiceOrderMapper;
|
|
@Resource
|
private ErpFinancePaymentMapper financePaymentMapper;
|
|
@Resource
|
private ErpNoRedisDAO noRedisDAO;
|
|
@Resource
|
private ErpPurchaseOrderService purchaseOrderService;
|
|
@Resource
|
private SrmSupplierApi srmSupplierApi;
|
|
@Resource
|
private StorageAttachmentApi storageAttachmentApi;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createPurchaseInvoice(ErpPurchaseInvoiceSaveReqVO createReqVO) {
|
// 1.1 校验采购订单、分摊明细(分摊合计 = 来票金额)
|
List<ErpPurchaseOrderDO> orders = validateOrderAllocations(createReqVO);
|
// 1.2 校验每张订单的分摊金额不超过该订单剩余未开票金额
|
validateAllocationPriceExceedsLimit(createReqVO, orders);
|
// 1.3 确定来票编号(前端填写则用自定义单号,留空则自动生成),并校验唯一性
|
String customNo = StrUtil.trim(createReqVO.getNo());
|
String no = StrUtil.isNotBlank(customNo) ? customNo
|
: 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);
|
invoiceMapper.insert(invoice);
|
|
// 2.2 插入采购订单分摊明细
|
insertOrderAllocations(invoice.getId(), createReqVO);
|
|
// 2.3 绑定发票附件
|
if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
|
storageAttachmentApi.bindAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, invoice.getId(), createReqVO.getBlobIds());
|
}
|
return invoice.getId();
|
}
|
|
/**
|
* 校验采购订单分摊明细,并把主订单、供应商回写到 reqVO 中
|
*
|
* @return 分摊明细中的采购订单列表(顺序与明细一致)
|
*/
|
private List<ErpPurchaseOrderDO> validateOrderAllocations(ErpPurchaseInvoiceSaveReqVO reqVO) {
|
List<ErpPurchaseInvoiceOrderReqVO> allocations = reqVO.getOrderAllocations();
|
if (CollUtil.isEmpty(allocations)) {
|
throw exception(PURCHASE_INVOICE_ALLOCATION_EMPTY);
|
}
|
// 1. 校验订单不重复、必须审核通过
|
Set<Long> orderIds = new HashSet<>();
|
List<ErpPurchaseOrderDO> orders = new ArrayList<>();
|
for (int i = 0; i < allocations.size(); i++) {
|
ErpPurchaseInvoiceOrderReqVO allocation = allocations.get(i);
|
if (!orderIds.add(allocation.getPurchaseOrderId())) {
|
throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_ORDER_DUPLICATE.getCode(),
|
StrUtil.format("第 {} 行的采购订单重复选择,同一张来票内每张采购订单只能出现一次,请删除重复行后重试", i + 1));
|
}
|
if (allocation.getPrice() == null || allocation.getPrice().compareTo(BigDecimal.ZERO) < 0) {
|
throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_PRICE_INVALID.getCode(),
|
StrUtil.format("第 {} 行的分摊金额不能为空且不能小于 0,请填写该订单本次分摊到的来票金额", i + 1));
|
}
|
orders.add(purchaseOrderService.validatePurchaseOrder(allocation.getPurchaseOrderId()));
|
}
|
// 1.2 校验所有采购订单必须是同一供应商(来票对应一个供应商)
|
validateSameSupplier(orders);
|
// 2. 校验分摊合计不能大于来票金额(允许小于:未分摊部分可后续编辑来票时再补充采购订单)
|
BigDecimal allocationTotal = allocations.stream().map(ErpPurchaseInvoiceOrderReqVO::getPrice)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
if (allocationTotal.compareTo(reqVO.getPrice()) > 0) {
|
throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_SUM_MISMATCH.getCode(),
|
StrUtil.format("来票金额 {} 元,各订单分摊合计 {} 元,分摊合计不能大于来票金额(超出 {} 元)。"
|
+ "请减少订单的分摊金额,或提高来票金额后重试;未分摊的部分可以留待以后再编辑来票补充采购订单。",
|
reqVO.getPrice().stripTrailingZeros().toPlainString(),
|
allocationTotal.stripTrailingZeros().toPlainString(),
|
allocationTotal.subtract(reqVO.getPrice()).stripTrailingZeros().toPlainString()));
|
}
|
// 3. 主订单、供应商取分摊明细第一张(一张来票涉及多个供应商时,列表/付款沿用第一家)
|
reqVO.setPurchaseOrderId(allocations.get(0).getPurchaseOrderId());
|
reqVO.setSupplierId(orders.get(0).getSupplierId());
|
return orders;
|
}
|
|
/**
|
* 校验分摊明细中的采购订单是否属于同一供应商:来票只能对应一个供应商
|
*/
|
private void validateSameSupplier(List<ErpPurchaseOrderDO> orders) {
|
ErpPurchaseOrderDO firstOrder = orders.get(0);
|
for (int i = 1; i < orders.size(); i++) {
|
ErpPurchaseOrderDO order = orders.get(i);
|
if (ObjectUtil.notEqual(order.getSupplierId(), firstOrder.getSupplierId())) {
|
throw new ServiceException(PURCHASE_INVOICE_ALLOCATION_SUPPLIER_MISMATCH.getCode(),
|
StrUtil.format("第 {} 行的采购订单[{}]供应商为[{}],与第 1 行采购订单[{}]的供应商[{}]不一致。"
|
+ "一张来票只能关联同一供应商的采购订单,请移除该订单,或改选该供应商的其他采购订单后重试。",
|
i + 1, order.getNo(), getSupplierName(order.getSupplierId()),
|
firstOrder.getNo(), getSupplierName(firstOrder.getSupplierId())));
|
}
|
}
|
}
|
|
private String getSupplierName(Long supplierId) {
|
SrmSupplierRespDTO supplier = srmSupplierApi.getSupplier(supplierId).getCheckedData();
|
return supplier == null ? "ID=" + supplierId : supplier.getName();
|
}
|
|
private void validateAllocationPriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO, List<ErpPurchaseOrderDO> orders) {
|
List<ErpPurchaseInvoiceOrderReqVO> allocations = reqVO.getOrderAllocations();
|
for (int i = 0; i < allocations.size(); i++) {
|
ErpPurchaseInvoiceOrderReqVO allocation = allocations.get(i);
|
ErpPurchaseOrderDO order = orders.get(i);
|
// 1. 该订单其他来票已占用的金额(编辑时排除本张来票自身的分摊)
|
List<ErpPurchaseInvoiceOrderDO> existed = invoiceOrderMapper.selectListByPurchaseOrderId(order.getId());
|
if (reqVO.getId() != null) {
|
existed.removeIf(row -> ObjectUtil.equal(row.getInvoiceId(), reqVO.getId()));
|
}
|
BigDecimal invoicedPrice = getSumValue(existed, ErpPurchaseInvoiceOrderDO::getPrice, BigDecimal::add, BigDecimal.ZERO);
|
BigDecimal remainPrice = order.getTotalPrice().subtract(invoicedPrice);
|
// 2. 校验金额是否超过
|
if (allocation.getPrice().compareTo(remainPrice) > 0) {
|
throw new ServiceException(PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT.getCode(),
|
StrUtil.format("采购订单[{}]的分摊金额超出该订单未开票金额:订单金额 {} 元,其他来票已占用 {} 元,"
|
+ "剩余未开票 {} 元,本次分摊 {} 元。请调整该订单的分摊金额,或改选其他采购订单后重试。",
|
order.getNo(), order.getTotalPrice().stripTrailingZeros().toPlainString(),
|
invoicedPrice.stripTrailingZeros().toPlainString(),
|
remainPrice.stripTrailingZeros().toPlainString(),
|
allocation.getPrice().stripTrailingZeros().toPlainString()));
|
}
|
}
|
}
|
|
private void insertOrderAllocations(Long invoiceId, ErpPurchaseInvoiceSaveReqVO reqVO) {
|
List<ErpPurchaseInvoiceOrderDO> rows = new ArrayList<>();
|
for (int i = 0; i < reqVO.getOrderAllocations().size(); i++) {
|
ErpPurchaseInvoiceOrderReqVO allocation = reqVO.getOrderAllocations().get(i);
|
rows.add(ErpPurchaseInvoiceOrderDO.builder().invoiceId(invoiceId)
|
.purchaseOrderId(allocation.getPurchaseOrderId())
|
.price(allocation.getPrice()).sort(i).build());
|
}
|
rows.forEach(invoiceOrderMapper::insert);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updatePurchaseInvoice(ErpPurchaseInvoiceSaveReqVO updateReqVO) {
|
Assert.notNull(updateReqVO.getId(), "来票编号不能为空");
|
// 1.1 校验存在
|
validatePurchaseInvoiceExists(updateReqVO.getId());
|
// 1.2 校验采购订单、分摊明细、金额上限
|
List<ErpPurchaseOrderDO> orders = validateOrderAllocations(updateReqVO);
|
validateAllocationPriceExceedsLimit(updateReqVO, orders);
|
|
// 2. 更新来票
|
ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class);
|
invoiceMapper.updateById(updateObj);
|
|
// 3. 重建采购订单分摊明细
|
invoiceOrderMapper.deleteByInvoiceId(updateReqVO.getId());
|
insertOrderAllocations(updateReqVO.getId(), updateReqVO);
|
|
// 4. 增量更新发票附件
|
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);
|
// 2.3 删除采购订单分摊明细(释放订单的已来票金额)
|
invoiceOrderMapper.deleteByInvoiceId(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) {
|
List<ErpPurchaseInvoiceOrderDO> allocations = invoiceOrderMapper.selectListByPurchaseOrderId(orderId);
|
if (CollUtil.isEmpty(allocations)) {
|
return Collections.emptyList();
|
}
|
return invoiceMapper.selectByIds(convertSet(allocations, ErpPurchaseInvoiceOrderDO::getInvoiceId));
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierId(Long supplierId) {
|
return invoiceMapper.selectListBySupplierId(supplierId);
|
}
|
|
@Override
|
public Map<Long, BigDecimal> getPurchaseInvoicePriceSumMapByOrderIds(Collection<Long> orderIds, Long excludeInvoiceId) {
|
if (CollUtil.isEmpty(orderIds)) {
|
return Collections.emptyMap();
|
}
|
List<ErpPurchaseInvoiceOrderDO> allocations = invoiceOrderMapper.selectListByPurchaseOrderIds(orderIds);
|
if (excludeInvoiceId != null) {
|
// 编辑来票时把本张来票已分摊的金额退回订单
|
allocations.removeIf(row -> ObjectUtil.equal(row.getInvoiceId(), excludeInvoiceId));
|
}
|
return allocations.stream().collect(Collectors.groupingBy(ErpPurchaseInvoiceOrderDO::getPurchaseOrderId,
|
Collectors.mapping(ErpPurchaseInvoiceOrderDO::getPrice, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceOrderDO> getPurchaseInvoiceOrderListByInvoiceId(Long invoiceId) {
|
return invoiceOrderMapper.selectListByInvoiceId(invoiceId);
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceOrderDO> getPurchaseInvoiceOrderListByInvoiceIds(Collection<Long> invoiceIds) {
|
if (CollUtil.isEmpty(invoiceIds)) {
|
return Collections.emptyList();
|
}
|
return invoiceOrderMapper.selectListByInvoiceIds(invoiceIds);
|
}
|
|
@Override
|
public List<ErpPurchaseInvoiceOrderDO> getPurchaseInvoiceOrderListByPurchaseOrderIds(Collection<Long> purchaseOrderIds) {
|
if (CollUtil.isEmpty(purchaseOrderIds)) {
|
return Collections.emptyList();
|
}
|
return invoiceOrderMapper.selectListByPurchaseOrderIds(purchaseOrderIds);
|
}
|
|
}
|