| | |
| | | import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportDuplicateVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportExcelVO; |
| | | import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderImportRespVO; |
| | | 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.erp.dal.dataobject.purchase.ErpPurchaseRequestDO; |
| | | import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceOrderMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderItemMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper; |
| | | import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper; |
| | |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.*; |
| | | 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.*; |
| | |
| | | private ErpPurchaseOrderMapper purchaseOrderMapper; |
| | | @Resource |
| | | private ErpPurchaseOrderItemMapper purchaseOrderItemMapper; |
| | | @Resource |
| | | private ErpPurchaseInvoiceOrderMapper invoiceOrderMapper; |
| | | |
| | | @Resource |
| | | private ErpNoRedisDAO noRedisDAO; |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList, |
| | | Boolean directApprove) { |
| | | Boolean directApprove, Boolean overwrite) { |
| | | ErpPurchaseOrderImportRespVO respVO = ErpPurchaseOrderImportRespVO.builder() |
| | | .createList(new ArrayList<>()) |
| | | .updateList(new ArrayList<>()) |
| | | .failureList(new LinkedHashMap<>()) |
| | | .duplicateList(new ArrayList<>()) |
| | | .build(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | return respVO; |
| | |
| | | }); |
| | | } |
| | | |
| | | // 2. 按“采购单编号”分组:同编号多行 = 一张订单的多条明细;编号为空则每行独立成单 |
| | | // 2. 预热系统内有效采购订单:按单号、按“供应商 + 明细”指纹建索引,用于识别重复导入 |
| | | List<ErpPurchaseOrderDO> existingOrders = purchaseOrderMapper.selectList(); |
| | | Map<Long, List<ErpPurchaseOrderItemDO>> existingItemsMap = new HashMap<>(); |
| | | if (CollUtil.isNotEmpty(existingOrders)) { |
| | | purchaseOrderItemMapper.selectListByOrderIds( |
| | | existingOrders.stream().map(ErpPurchaseOrderDO::getId).toList()) |
| | | .forEach(item -> existingItemsMap.computeIfAbsent(item.getOrderId(), k -> new ArrayList<>()).add(item)); |
| | | } |
| | | Map<String, ErpPurchaseOrderDO> existingNoMap = new HashMap<>(); |
| | | Map<String, List<ErpPurchaseOrderDO>> existingFingerprintMap = new HashMap<>(); |
| | | for (ErpPurchaseOrderDO order : existingOrders) { |
| | | if (StrUtil.isNotBlank(order.getNo())) { |
| | | existingNoMap.put(order.getNo(), order); |
| | | } |
| | | String fingerprint = buildOrderFingerprint(order.getSupplierId(), existingItemsMap.get(order.getId())); |
| | | if (fingerprint != null) { |
| | | existingFingerprintMap.computeIfAbsent(fingerprint, k -> new ArrayList<>()).add(order); |
| | | } |
| | | } |
| | | |
| | | // 3. 按“采购单编号”分组:同编号多行 = 一张订单的多条明细;编号为空则每行独立成单 |
| | | Map<String, List<ErpPurchaseOrderImportExcelVO>> orderGroups = new LinkedHashMap<>(); |
| | | for (int i = 0; i < importList.size(); i++) { |
| | | ErpPurchaseOrderImportExcelVO row = importList.get(i); |
| | |
| | | orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row); |
| | | } |
| | | |
| | | // 3. 逐个订单导入(单张订单失败不影响其它) |
| | | // 4. 逐个订单导入(单张订单失败不影响其它) |
| | | for (Map.Entry<String, List<ErpPurchaseOrderImportExcelVO>> entry : orderGroups.entrySet()) { |
| | | List<ErpPurchaseOrderImportExcelVO> rows = entry.getValue(); |
| | | String label = StrUtil.isNotBlank(rows.get(0).getNo()) ? rows.get(0).getNo().trim() |
| | | : "第 " + entry.getKey().substring("__row_".length()) + " 行"; |
| | | try { |
| | | ErpPurchaseOrderDO purchaseOrder = createPurchaseOrderByImport(rows, supplierIdMap, directApprove); |
| | | respVO.getCreateList().add(purchaseOrder.getNo()); |
| | | ImportOrderData importData = prepareImportOrder(rows, supplierIdMap); |
| | | |
| | | // 4.1 先按单号找;只有“单号为空、由系统生成编号”的行才按“供应商 + 完整明细”指纹找。 |
| | | // 文件里写了明确单号的,视为用户明确要新增该单号,指纹相同也不拦截,避免正常的重复采购被误判。 |
| | | ErpPurchaseOrderDO existing = importData.order.getNo() == null ? null |
| | | : existingNoMap.get(importData.order.getNo()); |
| | | String duplicateReason = existing != null ? "采购单编号已存在" : null; |
| | | boolean matchedByFingerprint = false; |
| | | List<ErpPurchaseOrderDO> fingerprintCandidates = null; |
| | | if (existing == null && importData.generatedNo && importData.fingerprint != null) { |
| | | fingerprintCandidates = existingFingerprintMap.get(importData.fingerprint); |
| | | if (CollUtil.isNotEmpty(fingerprintCandidates)) { |
| | | existing = fingerprintCandidates.get(0); |
| | | matchedByFingerprint = true; |
| | | duplicateReason = "供应商、明细、数量、单价、金额与已有订单一致"; |
| | | } |
| | | } |
| | | |
| | | // 4.2 发现重复:默认不导入,返回给前端弹框提示;勾选覆盖后才更新已有订单 |
| | | if (existing != null) { |
| | | if (!Boolean.TRUE.equals(overwrite)) { |
| | | respVO.getDuplicateList().add(buildImportDuplicate(label, existing, duplicateReason)); |
| | | continue; |
| | | } |
| | | if (matchedByFingerprint && fingerprintCandidates.size() > 1) { |
| | | String nos = fingerprintCandidates.stream().map(ErpPurchaseOrderDO::getNo) |
| | | .collect(Collectors.joining("、")); |
| | | throw new IllegalArgumentException("找到多张相同订单(" + nos |
| | | + "),无法自动覆盖,请先保留一张后再导入"); |
| | | } |
| | | updatePurchaseOrderByImport(existing, importData, directApprove); |
| | | respVO.getUpdateList().add(existing.getNo()); |
| | | continue; |
| | | } |
| | | |
| | | // 4.3 没有重复,正常新增 |
| | | createPurchaseOrderByImport(importData, directApprove); |
| | | respVO.getCreateList().add(importData.order.getNo()); |
| | | } catch (Exception ex) { |
| | | respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 依据导入数据创建一张采购订单(含明细)。 |
| | | * |
| | | * 注意:此处不调用 submitPurchaseOrder,因此不会创建 BPM 工作流;订单状态由 directApprove 直接决定: |
| | | * 勾选“是否审核” => 已审核(20),未勾选 => 未审核(10)。 |
| | | * 校验并组装一张导入订单(不落库)。 |
| | | */ |
| | | private ErpPurchaseOrderDO createPurchaseOrderByImport(List<ErpPurchaseOrderImportExcelVO> rows, |
| | | Map<String, Long> supplierIdMap, Boolean directApprove) { |
| | | private ImportOrderData prepareImportOrder(List<ErpPurchaseOrderImportExcelVO> rows, |
| | | Map<String, Long> supplierIdMap) { |
| | | // 1. 供应商(取该组首行) |
| | | ErpPurchaseOrderImportExcelVO head = rows.get(0); |
| | | if (StrUtil.isBlank(head.getSupplierName())) { |
| | |
| | | items.add(itemVO); |
| | | } |
| | | |
| | | // 3. 单号:填了就用(校验唯一),没填自动生成 |
| | | String no = StrUtil.isNotBlank(head.getNo()) ? head.getNo().trim() |
| | | : noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX); |
| | | if (purchaseOrderMapper.selectByNo(no) != null) { |
| | | throw new IllegalArgumentException("采购单编号已存在:" + no); |
| | | } |
| | | // 3. 单号:填了就用;留空时先不生成,等确认要新增时再生成,避免重复行被跳过也消耗流水号 |
| | | boolean generatedNo = StrUtil.isBlank(head.getNo()); |
| | | |
| | | // 4. 组装主表:勾选“是否审核” => 已审核(20),否则未审核(10) |
| | | // 4. 组装主表(状态由调用方决定) |
| | | ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO(); |
| | | purchaseOrder.setNo(no); |
| | | purchaseOrder.setNo(generatedNo ? null : head.getNo().trim()); |
| | | purchaseOrder.setSupplierId(supplierId); |
| | | purchaseOrder.setOrderTime(parseImportOrderTime(head.getOrderTime())); |
| | | purchaseOrder.setRemark(head.getRemark()); |
| | | purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove) |
| | | ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus()); |
| | | purchaseOrder.setDepositPrice(BigDecimal.ZERO); // deposit_price 为 NOT NULL 且无默认值,必须显式赋值 |
| | | purchaseOrder.setInCount(BigDecimal.ZERO); |
| | | purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()); |
| | | |
| | | List<ErpPurchaseOrderItemDO> itemDOs = validatePurchaseOrderItems(items); |
| | | calculateTotalPrice(purchaseOrder, itemDOs); |
| | | return new ImportOrderData(purchaseOrder, itemDOs, |
| | | buildOrderFingerprint(supplierId, itemDOs), generatedNo, |
| | | StrUtil.isNotBlank(head.getOrderTime())); |
| | | } |
| | | |
| | | /** |
| | | * 新增导入订单(含明细)。 |
| | | */ |
| | | private void createPurchaseOrderByImport(ImportOrderData importData, Boolean directApprove) { |
| | | ErpPurchaseOrderDO purchaseOrder = importData.order; |
| | | if (StrUtil.isBlank(purchaseOrder.getNo())) { |
| | | purchaseOrder.setNo(noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX)); |
| | | } |
| | | purchaseOrder.setStatus(Boolean.TRUE.equals(directApprove) |
| | | ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus()); |
| | | purchaseOrder.setInCount(BigDecimal.ZERO); |
| | | purchaseOrder.setInStatus(ErpPurchaseOrderInStatusEnum.NOT_IN.getStatus()); |
| | | purchaseOrderMapper.insert(purchaseOrder); |
| | | itemDOs.forEach(item -> item.setOrderId(purchaseOrder.getId())); |
| | | purchaseOrderItemMapper.insertBatch(itemDOs); |
| | | return purchaseOrder; |
| | | importData.items.forEach(item -> item.setOrderId(purchaseOrder.getId())); |
| | | purchaseOrderItemMapper.insertBatch(importData.items); |
| | | } |
| | | |
| | | /** |
| | | * 覆盖已有导入订单(含明细)。 |
| | | */ |
| | | private void updatePurchaseOrderByImport(ErpPurchaseOrderDO existing, ImportOrderData importData, |
| | | Boolean directApprove) { |
| | | // 1. 有下游单据的订单不允许覆盖,避免把已入库/已开票的数据改掉 |
| | | if (existing.getInCount() != null && existing.getInCount().compareTo(BigDecimal.ZERO) > 0) { |
| | | throw new IllegalArgumentException("采购订单[" + existing.getNo() + "]已入库(已入库数量 " |
| | | + normalizeNumber(existing.getInCount()) + "),不能覆盖;请先删除对应入库记录后再导入"); |
| | | } |
| | | if (existing.getReturnCount() != null && existing.getReturnCount().compareTo(BigDecimal.ZERO) > 0) { |
| | | throw new IllegalArgumentException("采购订单[" + existing.getNo() + "]已退货(已退货数量 " |
| | | + normalizeNumber(existing.getReturnCount()) + "),不能覆盖;请先删除对应退货记录后再导入"); |
| | | } |
| | | if (CollUtil.isNotEmpty(invoiceOrderMapper.selectListByPurchaseOrderId(existing.getId()))) { |
| | | throw new IllegalArgumentException("采购订单[" + existing.getNo() |
| | | + "]已关联发票,不能覆盖;请先解除发票关联后再导入"); |
| | | } |
| | | if (StrUtil.isNotBlank(existing.getProcessInstanceId())) { |
| | | throw new IllegalArgumentException("采购订单[" + existing.getNo() |
| | | + "]已提交审批流程,不能覆盖;请先在协同办公撤回或作废该审批后再导入"); |
| | | } |
| | | List<ErpPurchaseOrderItemDO> oldItems = purchaseOrderItemMapper.selectListByOrderId(existing.getId()); |
| | | boolean linkedRequest = oldItems.stream().anyMatch(item -> item.getRequestItemId() != null); |
| | | if (linkedRequest) { |
| | | throw new IllegalArgumentException("采购订单[" + existing.getNo() |
| | | + "]已关联采购申请,不能覆盖;请先在采购申请中撤销或删除对应申请后再导入"); |
| | | } |
| | | |
| | | // 2. 更新主表(保留原单号、原入库状态) |
| | | ErpPurchaseOrderDO updateObj = importData.order; |
| | | updateObj.setId(existing.getId()); |
| | | updateObj.setNo(existing.getNo()); |
| | | // 文件没填采购时间时保留原订单时间,避免覆盖时把日期改成当前时间 |
| | | if (!importData.orderTimeProvided) { |
| | | updateObj.setOrderTime(existing.getOrderTime()); |
| | | } |
| | | updateObj.setStatus(Boolean.TRUE.equals(directApprove) |
| | | ? ErpAuditStatus.APPROVE.getStatus() : ErpAuditStatus.PROCESS.getStatus()); |
| | | updateObj.setInCount(existing.getInCount() == null ? BigDecimal.ZERO : existing.getInCount()); |
| | | updateObj.setInStatus(existing.getInStatus()); |
| | | purchaseOrderMapper.updateById(updateObj); |
| | | |
| | | // 3. 明细整组替换 |
| | | purchaseOrderItemMapper.deleteByOrderId(existing.getId()); |
| | | importData.items.forEach(item -> item.setOrderId(existing.getId())); |
| | | purchaseOrderItemMapper.insertBatch(importData.items); |
| | | } |
| | | |
| | | private ErpPurchaseOrderImportDuplicateVO buildImportDuplicate(String label, ErpPurchaseOrderDO existing, |
| | | String reason) { |
| | | return ErpPurchaseOrderImportDuplicateVO.builder() |
| | | .key(label) |
| | | .existingNo(existing.getNo()) |
| | | .existingId(existing.getId()) |
| | | .reason(reason) |
| | | .build(); |
| | | } |
| | | |
| | | /** |
| | | * 生成“供应商 + 完整明细”的订单指纹,用于识别重复导入。 |
| | | * 明细按物料、数量、单价、税率排序后比较,不受行顺序影响。 |
| | | */ |
| | | private String buildOrderFingerprint(Long supplierId, List<ErpPurchaseOrderItemDO> items) { |
| | | if (supplierId == null || CollUtil.isEmpty(items)) { |
| | | return null; |
| | | } |
| | | List<String> parts = items.stream() |
| | | .map(item -> String.join(":", |
| | | String.valueOf(item.getProductId()), |
| | | normalizeNumber(item.getCount()), |
| | | normalizeNumber(item.getProductPrice()), |
| | | normalizeNumber(item.getTaxPercent()))) |
| | | .sorted() |
| | | .toList(); |
| | | return supplierId + "|" + String.join(",", parts); |
| | | } |
| | | |
| | | private String normalizeNumber(BigDecimal value) { |
| | | return value == null ? "" : value.stripTrailingZeros().toPlainString(); |
| | | } |
| | | |
| | | private static class ImportOrderData { |
| | | |
| | | private final ErpPurchaseOrderDO order; |
| | | private final List<ErpPurchaseOrderItemDO> items; |
| | | private final String fingerprint; |
| | | /** |
| | | * 采购单编号是否由系统自动生成(文件里留空) |
| | | */ |
| | | private final boolean generatedNo; |
| | | /** |
| | | * 文件里是否填写了采购时间 |
| | | */ |
| | | private final boolean orderTimeProvided; |
| | | |
| | | private ImportOrderData(ErpPurchaseOrderDO order, List<ErpPurchaseOrderItemDO> items, String fingerprint, |
| | | boolean generatedNo, boolean orderTimeProvided) { |
| | | this.order = order; |
| | | this.items = items; |
| | | this.fingerprint = fingerprint; |
| | | this.generatedNo = generatedNo; |
| | | this.orderTimeProvided = orderTimeProvided; |
| | | } |
| | | } |
| | | |
| | | /** |