| | |
| | | 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.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; |
| | |
| | | private ErpPurchaseInvoiceMapper invoiceMapper; |
| | | |
| | | @Resource |
| | | private ErpPurchaseInvoiceOrderMapper invoiceOrderMapper; |
| | | |
| | | @Resource |
| | | private ErpFinancePaymentMapper financePaymentMapper; |
| | | |
| | | @Resource |
| | |
| | | private ErpPurchaseOrderService purchaseOrderService; |
| | | |
| | | @Resource |
| | | private SrmSupplierApi srmSupplierApi; |
| | | |
| | | @Resource |
| | | private StorageAttachmentApi storageAttachmentApi; |
| | | |
| | | @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); |
| | | // 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 插入来票(创建后即为有效状态,无需审核) |
| | | // 2.1 插入来票(创建后即为有效状态,无需审核);主订单、供应商取分摊明细第一张 |
| | | ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no); |
| | | invoiceMapper.insert(invoice); |
| | | |
| | | // 2.2 绑定发票附件 |
| | | // 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(); |
| | | } |
| | | |
| | | 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 中 |
| | | * |
| | | * @return 分摊明细中的采购订单列表(顺序与明细一致) |
| | | */ |
| | | private List<ErpPurchaseOrderDO> validateOrderAllocations(ErpPurchaseInvoiceSaveReqVO reqVO) { |
| | | List<ErpPurchaseInvoiceOrderReqVO> allocations = reqVO.getOrderAllocations(); |
| | | if (CollUtil.isEmpty(allocations)) { |
| | | throw exception(PURCHASE_INVOICE_ALLOCATION_EMPTY); |
| | | } |
| | | reqVO.setSupplierId(purchaseOrder.getSupplierId()); // 供应商以采购订单为准 |
| | | // 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 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())); |
| | | /** |
| | | * 校验分摊明细中的采购订单是否属于同一供应商:来票只能对应一个供应商 |
| | | */ |
| | | 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()))); |
| | | } |
| | | } |
| | | 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); |
| | | } |
| | | |
| | | 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 |
| | |
| | | Assert.notNull(updateReqVO.getId(), "来票编号不能为空"); |
| | | // 1.1 校验存在 |
| | | validatePurchaseInvoiceExists(updateReqVO.getId()); |
| | | // 1.2 校验关联数据 + 可开票金额 |
| | | validateRelationDataExists(updateReqVO); |
| | | validatePurchaseInvoicePriceExceedsLimit(updateReqVO); |
| | | // 1.2 校验采购订单、分摊明细、金额上限 |
| | | List<ErpPurchaseOrderDO> orders = validateOrderAllocations(updateReqVO); |
| | | validateAllocationPriceExceedsLimit(updateReqVO, orders); |
| | | |
| | | // 2. 更新来票 |
| | | ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class); |
| | | invoiceMapper.updateById(updateObj); |
| | | |
| | | // 3. 增量更新发票附件 |
| | | // 3. 重建采购订单分摊明细 |
| | | invoiceOrderMapper.deleteByInvoiceId(updateReqVO.getId()); |
| | | insertOrderAllocations(updateReqVO.getId(), updateReqVO); |
| | | |
| | | // 4. 增量更新发票附件 |
| | | storageAttachmentApi.updateAttachments("file", PURCHASE_INVOICE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); |
| | | } |
| | | |
| | |
| | | storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id); |
| | | // 2.2 删除来票 |
| | | invoiceMapper.deleteById(id); |
| | | // 2.3 删除采购订单分摊明细(释放订单的已来票金额) |
| | | invoiceOrderMapper.deleteByInvoiceId(id); |
| | | } |
| | | |
| | | private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) { |
| | |
| | | |
| | | @Override |
| | | public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId) { |
| | | return invoiceMapper.selectListByOrderId(orderId); |
| | | List<ErpPurchaseInvoiceOrderDO> allocations = invoiceOrderMapper.selectListByPurchaseOrderId(orderId); |
| | | if (CollUtil.isEmpty(allocations)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return invoiceMapper.selectByIds(convertSet(allocations, ErpPurchaseInvoiceOrderDO::getInvoiceId)); |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | public Map<Long, BigDecimal> getPurchaseInvoicePriceSumMapByOrderIds(Collection<Long> orderIds) { |
| | | public Map<Long, BigDecimal> getPurchaseInvoicePriceSumMapByOrderIds(Collection<Long> orderIds, Long excludeInvoiceId) { |
| | | 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)))); |
| | | 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); |
| | | } |
| | | |
| | | } |