| | |
| | | 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 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; |
| | |
| | | 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) |
| | |
| | | |
| | | // 2.1 插入来票(创建后即为有效状态,无需审核) |
| | | ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no); |
| | | applyItemReceiptCode(invoice); |
| | | invoiceMapper.insert(invoice); |
| | | |
| | | // 2.2 绑定发票附件 |
| | |
| | | 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) { |
| | |
| | | } |
| | | BigDecimal remainPrice = purchaseOrder.getTotalPrice().subtract( |
| | | getSumValue(invoices, ErpPurchaseInvoiceDO::getPrice, BigDecimal::add, BigDecimal.ZERO)); |
| | | // 2. 校验金额是否超过 |
| | | // 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 |
| | |
| | | |
| | | // 2. 更新来票 |
| | | ErpPurchaseInvoiceDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInvoiceDO.class); |
| | | applyItemReceiptCode(updateObj); |
| | | invoiceMapper.updateById(updateObj); |
| | | |
| | | // 3. 增量更新发票附件 |
| | |
| | | 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(); |
| | | } |
| | | |
| | | } |