| | |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.number.MoneyUtils; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | |
| | | 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.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.enums.ErpAuditStatus; |
| | | import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderInStatusEnum; |
| | | import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService; |
| | | import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; |
| | | import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi; |
| | | import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO; |
| | | import cn.iocoder.yudao.module.mes.api.arrivalnotice.MesArrivalNoticeApi; |
| | | import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; |
| | | import jakarta.annotation.Resource; |
| | |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.*; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*; |
| | | |
| | | /** |
| | | * ERP 采购订单 Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | /** |
| | | * BPM 采购订单审批分类编码 |
| | | */ |
| | | private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_request_approve"; |
| | | private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_order_approve"; |
| | | /** |
| | | * BPM 采购订单审批流程定义 Key(创建订单后自动提交到协同办公审批) |
| | | */ |
| | | private static final String PURCHASE_ORDER_APPROVE_PROCESS_DEFINITION_KEY = "purchase_order_approve"; |
| | | |
| | | @Resource |
| | | private ErpPurchaseOrderMapper purchaseOrderMapper; |
| | |
| | | @Resource |
| | | private StorageAttachmentApi storageAttachmentApi; |
| | | |
| | | @Resource |
| | | private SrmSupplierApi srmSupplierApi; |
| | | @Resource |
| | | private MdmItemApi mdmItemApi; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createPurchaseOrder(ErpPurchaseOrderSaveReqVO createReqVO) { |
| | |
| | | // 2.3 绑定附件 |
| | | storageAttachmentApi.bindAttachments("file", "erp_purchase_order", |
| | | purchaseOrder.getId(), createReqVO.getBlobIds()); |
| | | |
| | | // 3. 创建后自动提交审批到协同办公(审批在协同办公完成,不在本页面审批) |
| | | submitPurchaseOrder(purchaseOrder.getId(), PURCHASE_ORDER_APPROVE_PROCESS_DEFINITION_KEY, getLoginUserId()); |
| | | |
| | | return purchaseOrder.getId(); |
| | | } |
| | | |
| | |
| | | updatePurchaseOrderItemList(updateReqVO.getId(), purchaseOrderItems); |
| | | // 2.3 更新附件 |
| | | storageAttachmentApi.updateAttachments("file", "erp_purchase_order", updateReqVO.getId(), updateReqVO.getBlobIds()); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public ErpPurchaseOrderImportRespVO importPurchaseOrderList(List<ErpPurchaseOrderImportExcelVO> importList, |
| | | Boolean directApprove) { |
| | | ErpPurchaseOrderImportRespVO respVO = ErpPurchaseOrderImportRespVO.builder() |
| | | .createList(new ArrayList<>()) |
| | | .updateList(new ArrayList<>()) |
| | | .failureList(new LinkedHashMap<>()) |
| | | .build(); |
| | | if (CollUtil.isEmpty(importList)) { |
| | | return respVO; |
| | | } |
| | | |
| | | // 1. 预热“供应商名称/编码 -> 供应商编号”映射(ERP 供应商主数据在 SRM 模块) |
| | | Map<String, Long> supplierIdMap = new HashMap<>(); |
| | | CommonResult<List<SrmSupplierRespDTO>> supplierResult = srmSupplierApi.getSupplierSimpleList(); |
| | | if (supplierResult != null && CollUtil.isNotEmpty(supplierResult.getData())) { |
| | | supplierResult.getData().forEach(supplier -> { |
| | | if (StrUtil.isNotBlank(supplier.getName())) { |
| | | supplierIdMap.put(supplier.getName().trim(), supplier.getId()); |
| | | } |
| | | if (StrUtil.isNotBlank(supplier.getCode())) { |
| | | supplierIdMap.putIfAbsent(supplier.getCode().trim(), supplier.getId()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // 2. 按“采购单编号”分组:同编号多行 = 一张订单的多条明细;编号为空则每行独立成单 |
| | | Map<String, List<ErpPurchaseOrderImportExcelVO>> orderGroups = new LinkedHashMap<>(); |
| | | for (int i = 0; i < importList.size(); i++) { |
| | | ErpPurchaseOrderImportExcelVO row = importList.get(i); |
| | | if (row == null || (StrUtil.isBlank(row.getSupplierName()) && StrUtil.isBlank(row.getProductCode()) |
| | | && StrUtil.isBlank(row.getNo()))) { |
| | | continue; // 跳过空行 |
| | | } |
| | | String key = StrUtil.isNotBlank(row.getNo()) ? row.getNo().trim() : "__row_" + (i + 2); |
| | | orderGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(row); |
| | | } |
| | | |
| | | // 3. 逐个订单导入(单张订单失败不影响其它) |
| | | 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()); |
| | | } catch (Exception ex) { |
| | | respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); |
| | | } |
| | | } |
| | | return respVO; |
| | | } |
| | | |
| | | /** |
| | | * 依据导入数据创建一张采购订单(含明细)。 |
| | | * |
| | | * 注意:此处不调用 submitPurchaseOrder,因此不会创建 BPM 工作流;订单状态由 directApprove 直接决定: |
| | | * 勾选“是否审核” => 已审核(20),未勾选 => 未审核(10)。 |
| | | */ |
| | | private ErpPurchaseOrderDO createPurchaseOrderByImport(List<ErpPurchaseOrderImportExcelVO> rows, |
| | | Map<String, Long> supplierIdMap, Boolean directApprove) { |
| | | // 1. 供应商(取该组首行) |
| | | ErpPurchaseOrderImportExcelVO head = rows.get(0); |
| | | if (StrUtil.isBlank(head.getSupplierName())) { |
| | | throw new IllegalArgumentException("供应商名称不能为空"); |
| | | } |
| | | Long supplierId = supplierIdMap.get(head.getSupplierName().trim()); |
| | | if (supplierId == null) { |
| | | throw new IllegalArgumentException("供应商不存在:" + head.getSupplierName()); |
| | | } |
| | | supplierService.validateSupplier(supplierId); |
| | | |
| | | // 2. 明细 |
| | | List<ErpPurchaseOrderSaveReqVO.Item> items = new ArrayList<>(); |
| | | for (ErpPurchaseOrderImportExcelVO row : rows) { |
| | | if (StrUtil.isBlank(row.getProductCode())) { |
| | | throw new IllegalArgumentException("产品编码不能为空"); |
| | | } |
| | | CommonResult<MdmItemRespDTO> itemResult = mdmItemApi.getItemByCode(row.getProductCode().trim()); |
| | | MdmItemRespDTO item = itemResult == null ? null : itemResult.getData(); |
| | | if (item == null) { |
| | | throw new IllegalArgumentException("产品不存在:" + row.getProductCode()); |
| | | } |
| | | if (row.getCount() == null) { |
| | | throw new IllegalArgumentException("产品[" + row.getProductCode() + "]的数量不能为空"); |
| | | } |
| | | if (item.getUnitMeasureId() == null) { |
| | | throw new IllegalArgumentException("产品[" + row.getProductCode() + "]未配置计量单位,请先在物料中维护"); |
| | | } |
| | | ErpPurchaseOrderSaveReqVO.Item itemVO = new ErpPurchaseOrderSaveReqVO.Item(); |
| | | itemVO.setProductId(item.getId()); |
| | | itemVO.setProductUnitId(item.getUnitMeasureId()); |
| | | itemVO.setProductPrice(row.getProductPrice() == null ? BigDecimal.ZERO : row.getProductPrice()); |
| | | itemVO.setCount(row.getCount()); |
| | | itemVO.setTaxPercent(row.getTaxPercent()); |
| | | itemVO.setRemark(row.getRemark()); |
| | | itemVO.setQcCheckFlag("是".equals(StrUtil.trim(row.getQcCheckFlag()))); |
| | | 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); |
| | | } |
| | | |
| | | // 4. 组装主表:勾选“是否审核” => 已审核(20),否则未审核(10) |
| | | ErpPurchaseOrderDO purchaseOrder = new ErpPurchaseOrderDO(); |
| | | purchaseOrder.setNo(no); |
| | | 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); |
| | | purchaseOrderMapper.insert(purchaseOrder); |
| | | itemDOs.forEach(item -> item.setOrderId(purchaseOrder.getId())); |
| | | purchaseOrderItemMapper.insertBatch(itemDOs); |
| | | return purchaseOrder; |
| | | } |
| | | |
| | | /** |
| | | * 解析导入的采购时间,兼容多种常见格式;为空取当前时间 |
| | | */ |
| | | private LocalDateTime parseImportOrderTime(String text) { |
| | | if (StrUtil.isBlank(text)) { |
| | | return LocalDateTime.now(); |
| | | } |
| | | String value = text.trim(); |
| | | for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) { |
| | | try { |
| | | return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern)); |
| | | } catch (Exception ignored) { |
| | | // 继续尝试下一个格式 |
| | | } |
| | | } |
| | | for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) { |
| | | try { |
| | | return LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay(); |
| | | } catch (Exception ignored) { |
| | | // 继续尝试下一个格式 |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("采购时间格式不正确:" + text + ",示例 2026-01-01 10:00:00"); |
| | | } |
| | | |
| | | /** |
| | | * 提取导入失败原因(ServiceException 取业务提示) |
| | | */ |
| | | private String resolveImportErrorMessage(Exception ex) { |
| | | if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) { |
| | | return serviceException.getMessage(); |
| | | } |
| | | return StrUtil.blankToDefault(ex.getMessage(), "导入失败"); |
| | | } |
| | | |
| | | private void calculateTotalPrice(ErpPurchaseOrderDO purchaseOrder, List<ErpPurchaseOrderItemDO> purchaseOrderItems) { |
| | |
| | | if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) { |
| | | return ErpAuditStatus.APPROVE.getStatus(); |
| | | } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) { |
| | | return ErpAuditStatus.PROCESS.getStatus(); |
| | | return ErpAuditStatus.REJECT.getStatus(); |
| | | } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) { |
| | | return ErpAuditStatus.CANCEL.getStatus(); |
| | | } |
| | |
| | | } |
| | | |
| | | @Override |
| | | public ErpPurchaseOrderDO getPurchaseOrderByProcessInstanceId(String processInstanceId) { |
| | | return purchaseOrderMapper.selectByProcessInstanceId(processInstanceId); |
| | | } |
| | | |
| | | @Override |
| | | public ErpPurchaseOrderDO validatePurchaseOrder(Long id) { |
| | | ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id); |
| | | if (ObjectUtil.notEqual(purchaseOrder.getStatus(), ErpAuditStatus.APPROVE.getStatus())) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | public List<ErpPurchaseOrderDO> getPurchaseOrderListBySource(Integer sourceType, Long sourceId) { |
| | | if (sourceType == null || sourceId == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return purchaseOrderMapper.selectListBySource(sourceType, sourceId); |
| | | } |
| | | |
| | | @Override |
| | | public void updatePurchaseOrderInStatus(Long orderId) { |
| | | // 1. 获取采购订单 |
| | | ErpPurchaseOrderDO order = validatePurchaseOrderExists(orderId); |