| | |
| | | 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.CrmReceivableDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivableItemDO; |
| | | import cn.iocoder.yudao.module.crm.dal.dataobject.receivable.CrmReceivablePlanDO; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableItemMapper; |
| | | import cn.iocoder.yudao.module.crm.dal.mysql.receivable.CrmReceivableMapper; |
| | | 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.CrmReceivableBizTypeEnum; |
| | | 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.invoice.CrmInvoiceService; |
| | |
| | | |
| | | @Resource |
| | | private CrmReceivableMapper receivableMapper; |
| | | @Resource |
| | | private CrmReceivableItemMapper receivableItemMapper; |
| | | |
| | | @Resource |
| | | private ErpSaleOrderApi saleOrderApi; |
| | | @Resource |
| | | private MesWmProductSalesApi productSalesApi; |
| | | |
| | | @Resource |
| | | private CrmNoRedisDAO noRedisDAO; |
| | |
| | | CrmReceivableDO receivable = BeanUtils.toBean(createReqVO, CrmReceivableDO.class) |
| | | .setNo(no).setAuditStatus(CrmAuditStatusEnum.PROCESS.getStatus()); |
| | | receivableMapper.insert(receivable); |
| | | // 2.2 |
| | | // 2.2 保存核销明细(未传时按关联开票自动生成一条) |
| | | syncReceivableItems(receivable, createReqVO.getItems()); |
| | | |
| | | // 3. 创建数据权限 |
| | | permissionService.createPermission(new CrmPermissionCreateReqBO().setBizType(CrmBizTypeEnum.CRM_RECEIVABLE.getType()) |
| | |
| | | LogRecordContext.putVariable("receivable", receivable); |
| | | LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId())); |
| | | return receivable.getId(); |
| | | } |
| | | |
| | | /** |
| | | * 保存核销明细 |
| | | * |
| | | * 传 null 时:若关联开票,自动生成一条「开票」核销明细;否则不生成。 |
| | | */ |
| | | private void syncReceivableItems(CrmReceivableDO receivable, |
| | | List<CrmReceivableSaveReqVO.ReceivableItem> items) { |
| | | if (items == null) { |
| | | if (receivable.getInvoiceId() == null) { |
| | | return; |
| | | } |
| | | CrmReceivableSaveReqVO.ReceivableItem autoItem = new CrmReceivableSaveReqVO.ReceivableItem(); |
| | | autoItem.setBizType(CrmReceivableBizTypeEnum.SALE_INVOICE.getType()); |
| | | autoItem.setBizId(receivable.getInvoiceId()); |
| | | autoItem.setReceiptPrice(receivable.getPrice()); |
| | | items = List.of(autoItem); |
| | | } |
| | | // 1. 校验并解析每行关联单据 |
| | | List<ResolvedReceivableItem> resolvedItems = new ArrayList<>(items.size()); |
| | | BigDecimal itemPriceSum = BigDecimal.ZERO; |
| | | for (int i = 0; i < items.size(); i++) { |
| | | CrmReceivableSaveReqVO.ReceivableItem item = items.get(i); |
| | | int row = i + 1; |
| | | CrmReceivableBizTypeEnum bizType = item == null ? null |
| | | : CrmReceivableBizTypeEnum.valueOf(item.getBizType()); |
| | | if (bizType == null) { |
| | | throw exception(RECEIVABLE_ITEM_BIZ_TYPE_INVALID, row, |
| | | item == null ? "未填写" : item.getBizType()); |
| | | } |
| | | if (item.getReceiptPrice() == null || item.getReceiptPrice().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw exception(RECEIVABLE_ITEM_PRICE_INVALID, row, bizType.getName(), |
| | | item.getBizId(), item.getReceiptPrice() == null ? "未填写" |
| | | : item.getReceiptPrice().stripTrailingZeros().toPlainString()); |
| | | } |
| | | ResolvedReceivableItem resolved = resolveReceivableItem(row, bizType, item.getBizId()); |
| | | // 1.1 已核销金额(不含本回款单,避免编辑时把自己算进去) |
| | | BigDecimal receiptedPrice = receivableItemMapper.selectListByBiz(bizType.getType(), item.getBizId()).stream() |
| | | .filter(old -> ObjectUtil.notEqual(old.getReceivableId(), receivable.getId())) |
| | | .map(old -> old.getReceiptPrice() == null ? BigDecimal.ZERO : old.getReceiptPrice()) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | BigDecimal remainPrice = resolved.totalPrice.subtract(receiptedPrice); |
| | | if (item.getReceiptPrice().compareTo(remainPrice) > 0) { |
| | | throw exception(RECEIVABLE_ITEM_PRICE_EXCEEDS, row, bizType.getName(), resolved.bizNo, |
| | | item.getReceiptPrice().stripTrailingZeros().toPlainString(), |
| | | remainPrice.stripTrailingZeros().toPlainString(), |
| | | resolved.totalPrice.stripTrailingZeros().toPlainString(), |
| | | receiptedPrice.stripTrailingZeros().toPlainString()); |
| | | } |
| | | resolved.receiptedPrice = receiptedPrice; |
| | | resolved.receiptPrice = item.getReceiptPrice(); |
| | | resolved.remark = item.getRemark(); |
| | | resolvedItems.add(resolved); |
| | | itemPriceSum = itemPriceSum.add(item.getReceiptPrice()); |
| | | } |
| | | // 2. 明细合计必须等于回款金额 |
| | | if (itemPriceSum.compareTo(receivable.getPrice()) != 0) { |
| | | throw exception(RECEIVABLE_ITEM_SUM_MISMATCH, |
| | | itemPriceSum.stripTrailingZeros().toPlainString(), |
| | | receivable.getPrice().stripTrailingZeros().toPlainString()); |
| | | } |
| | | // 3. 重建明细 |
| | | receivableItemMapper.deleteByReceivableId(receivable.getId()); |
| | | for (ResolvedReceivableItem resolved : resolvedItems) { |
| | | receivableItemMapper.insert(CrmReceivableItemDO.builder() |
| | | .receivableId(receivable.getId()).bizType(resolved.bizType.getType()) |
| | | .bizId(resolved.bizId).bizNo(resolved.bizNo).totalPrice(resolved.totalPrice) |
| | | .receiptedPrice(resolved.receiptedPrice).receiptPrice(resolved.receiptPrice) |
| | | .remark(resolved.remark).build()); |
| | | } |
| | | } |
| | | |
| | | private ResolvedReceivableItem resolveReceivableItem(int row, CrmReceivableBizTypeEnum bizType, Long bizId) { |
| | | ResolvedReceivableItem resolved = new ResolvedReceivableItem(); |
| | | resolved.bizType = bizType; |
| | | resolved.bizId = bizId; |
| | | switch (bizType) { |
| | | case SALE_ORDER -> { |
| | | ErpSaleOrderRespDTO saleOrder = bizId == null ? null : saleOrderApi.getSaleOrder(bizId).getCheckedData(); |
| | | if (saleOrder == null) { |
| | | throw exception(RECEIVABLE_ITEM_BIZ_NOT_EXISTS, row, bizType.getName(), bizId); |
| | | } |
| | | resolved.bizNo = saleOrder.getNo(); |
| | | resolved.totalPrice = saleOrder.getTotalPrice() == null ? BigDecimal.ZERO : saleOrder.getTotalPrice(); |
| | | } |
| | | case SALE_OUT -> { |
| | | MesWmProductSalesRespDTO saleOut = bizId == null ? null : productSalesApi.getProductSales(bizId); |
| | | if (saleOut == null) { |
| | | throw exception(RECEIVABLE_ITEM_BIZ_NOT_EXISTS, row, bizType.getName(), bizId); |
| | | } |
| | | resolved.bizNo = saleOut.getCode(); |
| | | resolved.totalPrice = saleOut.getTotalPrice() == null ? BigDecimal.ZERO : saleOut.getTotalPrice(); |
| | | } |
| | | case SALE_INVOICE -> { |
| | | CrmInvoiceDO invoice = bizId == null ? null : invoiceService.getInvoice(bizId); |
| | | if (invoice == null) { |
| | | throw exception(RECEIVABLE_ITEM_BIZ_NOT_EXISTS, row, bizType.getName(), bizId); |
| | | } |
| | | resolved.bizNo = invoice.getNo(); |
| | | resolved.totalPrice = invoice.getPrice() == null ? BigDecimal.ZERO : invoice.getPrice(); |
| | | } |
| | | default -> throw exception(RECEIVABLE_ITEM_BIZ_TYPE_INVALID, row, bizType.getType()); |
| | | } |
| | | return resolved; |
| | | } |
| | | |
| | | private static class ResolvedReceivableItem { |
| | | private CrmReceivableBizTypeEnum bizType; |
| | | private Long bizId; |
| | | private String bizNo; |
| | | private BigDecimal totalPrice; |
| | | private BigDecimal receiptedPrice; |
| | | private BigDecimal receiptPrice; |
| | | private String remark; |
| | | } |
| | | |
| | | private void validateReceivablePriceExceedsLimit(CrmReceivableSaveReqVO reqVO) { |
| | |
| | | // 2. 更新回款 |
| | | CrmReceivableDO updateObj = BeanUtils.toBean(updateReqVO, CrmReceivableDO.class); |
| | | receivableMapper.updateById(updateObj); |
| | | // 2.1 更新核销明细:传了 items 以 items 为准;未传且关联开票时按开票重建;否则保持不变 |
| | | if (updateReqVO.getItems() != null |
| | | || (updateReqVO.getInvoiceId() != null && ObjectUtil.notEqual(updateReqVO.getInvoiceId(), oldReceivable.getInvoiceId()))) { |
| | | syncReceivableItems(updateObj, updateReqVO.getItems()); |
| | | } |
| | | |
| | | // 3. 记录操作日志上下文 |
| | | updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 |
| | |
| | | if (updateCount == 0) { |
| | | throw exception(approve ? RECEIVABLE_APPROVE_FAIL : RECEIVABLE_PROCESS_FAIL); |
| | | } |
| | | |
| | | // 3. 回款计划回写实际回款金额与状态 |
| | | if (receivable.getPlanId() != null) { |
| | | if (approve) { |
| | | receivablePlanService.updateReceivablePlanReceipted(receivable.getPlanId(), |
| | | receivable.getPrice(), receivable.getReturnTime()); |
| | | } else { |
| | | receivablePlanService.resetReceivablePlanReceipted(receivable.getPlanId()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | // 2.1 删除回款 |
| | | receivableMapper.deleteById(id); |
| | | // 2.2 删除数据权限 |
| | | // 2.2 删除核销明细 |
| | | receivableItemMapper.deleteByReceivableId(id); |
| | | // 2.3 删除数据权限 |
| | | permissionService.deletePermission(CrmBizTypeEnum.CRM_RECEIVABLE.getType(), id); |
| | | |
| | | // 3. 记录操作日志上下文 |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<CrmReceivableItemDO> getReceivableItemList(Long receivableId) { |
| | | return receivableItemMapper.selectListByReceivableId(receivableId); |
| | | } |
| | | |
| | | @Override |
| | | public List<CrmReceivableItemDO> getReceivableItemListByCustomerId(Long customerId) { |
| | | List<CrmReceivableDO> receivables = receivableMapper.selectListByCustomerId(customerId); |
| | | if (CollUtil.isEmpty(receivables)) { |
| | | return ListUtil.empty(); |
| | | } |
| | | return receivableItemMapper.selectListByReceivableIds( |
| | | receivables.stream().map(CrmReceivableDO::getId).toList()); |
| | | } |
| | | |
| | | @Override |
| | | @CrmPermission(bizType = CrmBizTypeEnum.CRM_CUSTOMER, bizId = "#pageReqVO.customerId", level = CrmPermissionLevelEnum.READ) |
| | | public PageResult<CrmReceivableDO> getReceivablePageByCustomerId(CrmReceivablePageReqVO pageReqVO) { |
| | | return receivableMapper.selectPageByCustomerId(pageReqVO); |