| | |
| | | package com.ruoyi.sales.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
| | |
| | | import com.ruoyi.account.service.AccountIncomeService; |
| | | import com.ruoyi.approve.service.IApproveProcessService; |
| | | import com.ruoyi.approve.vo.ApproveProcessVO; |
| | | import com.fasterxml.jackson.databind.JsonNode; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.fasterxml.jackson.databind.node.ObjectNode; |
| | | import com.ruoyi.approve.pojo.ApproveProcess; |
| | | import com.ruoyi.common.enums.ApproveTypeEnum; |
| | | import com.ruoyi.basic.mapper.CustomerMapper; |
| | |
| | | } |
| | | return productModel.getUnit(); |
| | | } |
| | | |
| | | private static final String SCAN_SHIP_REMARK_PREFIX = "SCAN_SHIP_DELIVERY_JSON:"; |
| | | |
| | | private static final class ScanShipPayload { |
| | | private String shippingNo; |
| | | private Long ledgerId; |
| | | private String car; |
| | | private String express; |
| | | private String shipType; |
| | | private Map<Long, BigDecimal> linesQty = new LinkedHashMap<>(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void scanShipApply(SalesScanShipDto dto) { |
| | | if (dto == null || dto.getSalesLedgerId() == null) { |
| | | throw new ServiceException("扫码发货失败,订单不能为空"); |
| | | } |
| | | if (StringUtils.isEmpty(dto.getApproveUserIds())) { |
| | | throw new ServiceException("请选择审批人"); |
| | | } |
| | | if (CollectionUtils.isEmpty(dto.getSalesLedgerProductList())) { |
| | | throw new ServiceException("请填写发货产品行"); |
| | | } |
| | | String shipType = StringUtils.hasText(dto.getShipType()) ? dto.getShipType().trim() : "货车"; |
| | | if ("货车".equals(shipType)) { |
| | | if (!StringUtils.hasText(dto.getShippingCarNumber())) { |
| | | throw new ServiceException("请填写车牌号"); |
| | | } |
| | | } else if ("快递".equals(shipType)) { |
| | | if (!StringUtils.hasText(dto.getExpressNumber())) { |
| | | throw new ServiceException("请填写快递单号"); |
| | | } |
| | | } |
| | | SalesLedger salesLedger = baseMapper.selectById(dto.getSalesLedgerId()); |
| | | if (salesLedger == null) { |
| | | throw new ServiceException("销售订单不存在"); |
| | | } |
| | | if (salesLedger.getDeliveryStatus() != null && salesLedger.getDeliveryStatus() == 5) { |
| | | throw new ServiceException("该销售订单已发货"); |
| | | } |
| | | if (salesLedger.getDeliveryStatus() != null && salesLedger.getDeliveryStatus() == 2) { |
| | | throw new ServiceException("该销售订单已发起发货审批,请先完成审批"); |
| | | } |
| | | List<SalesLedgerProduct> notStocked = salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>() |
| | | .eq(SalesLedgerProduct::getSalesLedgerId, salesLedger.getId()) |
| | | .eq(SalesLedgerProduct::getType, 1) |
| | | .ne(SalesLedgerProduct::getProductStockStatus, 2)); |
| | | if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(notStocked)) { |
| | | throw new ServiceException("发货失败,该销售订单存在未入库产品,请先完成全部入库后再发货"); |
| | | } |
| | | int saleType = SaleEnum.SALE.getCode(); |
| | | Map<Long, BigDecimal> shipQtyByLineId = new LinkedHashMap<>(); |
| | | for (SalesLedgerProduct line : dto.getSalesLedgerProductList()) { |
| | | if (line == null || line.getId() == null) { |
| | | throw new ServiceException("产品信息不完整"); |
| | | } |
| | | BigDecimal q = line.getStockedQuantity(); |
| | | if (q == null) { |
| | | throw new ServiceException("发货数量不能为空"); |
| | | } |
| | | if (q.compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new ServiceException("发货数量不能为负数"); |
| | | } |
| | | shipQtyByLineId.merge(line.getId(), q, BigDecimal::add); |
| | | } |
| | | boolean anyPositive = shipQtyByLineId.values().stream().anyMatch(v -> v.compareTo(BigDecimal.ZERO) > 0); |
| | | if (!anyPositive) { |
| | | throw new ServiceException("请至少填写一行大于 0 的发货数量"); |
| | | } |
| | | Long ledgerId = salesLedger.getId(); |
| | | for (Map.Entry<Long, BigDecimal> entry : shipQtyByLineId.entrySet()) { |
| | | if (entry.getValue().compareTo(BigDecimal.ZERO) == 0) { |
| | | continue; |
| | | } |
| | | SalesLedgerProduct dbProduct = salesLedgerProductMapper.selectById(entry.getKey()); |
| | | if (dbProduct == null) { |
| | | throw new ServiceException("销售产品不存在"); |
| | | } |
| | | if (!Objects.equals(dbProduct.getSalesLedgerId(), ledgerId) || !Objects.equals(dbProduct.getType(), saleType)) { |
| | | throw new ServiceException("销售产品与订单不匹配"); |
| | | } |
| | | if (dbProduct.getProductModelId() == null) { |
| | | throw new ServiceException("产品规格未维护,无法发货"); |
| | | } |
| | | BigDecimal orderQty = defaultDecimal(dbProduct.getQuantity()); |
| | | BigDecimal prevShipped = defaultDecimal(dbProduct.getShippedQuantity()); |
| | | if (prevShipped.add(entry.getValue()).compareTo(orderQty) > 0) { |
| | | throw new ServiceException("累计发货数量不能大于该产品订单数量"); |
| | | } |
| | | stockUtils.assertQualifiedAvailable(dbProduct.getProductModelId(), entry.getValue()); |
| | | } |
| | | String shNo = OrderUtils.countTodayByCreateTime(shippingInfoMapper, "SH"); |
| | | Map<Long, BigDecimal> positiveLines = new LinkedHashMap<>(); |
| | | for (Map.Entry<Long, BigDecimal> e : shipQtyByLineId.entrySet()) { |
| | | if (e.getValue().compareTo(BigDecimal.ZERO) > 0) { |
| | | positiveLines.put(e.getKey(), e.getValue()); |
| | | } |
| | | } |
| | | for (Map.Entry<Long, BigDecimal> e : positiveLines.entrySet()) { |
| | | ShippingInfo si = new ShippingInfo(); |
| | | si.setSalesLedgerId(ledgerId); |
| | | si.setSalesLedgerProductId(e.getKey()); |
| | | si.setShippingNo(shNo); |
| | | si.setStatus("待审核"); |
| | | si.setType(shipType); |
| | | if ("货车".equals(shipType)) { |
| | | si.setShippingCarNumber(dto.getShippingCarNumber().trim()); |
| | | } |
| | | if ("快递".equals(shipType)) { |
| | | si.setExpressNumber(dto.getExpressNumber().trim()); |
| | | } |
| | | shippingInfoMapper.insert(si); |
| | | } |
| | | String remarkJson = buildScanShipRemarkJson(shNo, ledgerId, dto, positiveLines); |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | ApproveProcessVO approveProcessVO = new ApproveProcessVO(); |
| | | approveProcessVO.setApproveType(7); |
| | | approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId()); |
| | | approveProcessVO.setApproveReason("发货审批:" + salesLedger.getSalesContractNo()); |
| | | approveProcessVO.setApproveRemark(remarkJson); |
| | | approveProcessVO.setApproveUserIds(dto.getApproveUserIds().trim()); |
| | | approveProcessVO.setApproveUser(loginUser.getUserId()); |
| | | approveProcessVO.setApproveTime(LocalDate.now().toString()); |
| | | approveProcessVO.setTempFileIds(dto.getTempFileIds()); |
| | | try { |
| | | approveProcessService.addApprove(approveProcessVO); |
| | | } catch (Exception e) { |
| | | throw new ServiceException("发起发货审批失败: " + e.getMessage()); |
| | | } |
| | | salesLedger.setDeliveryStatus(2); |
| | | baseMapper.updateById(salesLedger); |
| | | } |
| | | |
| | | private String buildScanShipRemarkJson(String shippingNo, Long ledgerId, SalesScanShipDto dto, Map<Long, BigDecimal> lines) { |
| | | try { |
| | | ObjectMapper om = new ObjectMapper(); |
| | | ObjectNode root = om.createObjectNode(); |
| | | root.put("shippingNo", shippingNo); |
| | | root.put("ledgerId", ledgerId); |
| | | root.put("car", dto.getShippingCarNumber() == null ? "" : dto.getShippingCarNumber().trim()); |
| | | root.put("express", dto.getExpressNumber() == null ? "" : dto.getExpressNumber().trim()); |
| | | root.put("shipType", dto.getShipType() == null ? "货车" : dto.getShipType().trim()); |
| | | ObjectNode linesNode = om.createObjectNode(); |
| | | for (Map.Entry<Long, BigDecimal> e : lines.entrySet()) { |
| | | linesNode.put(String.valueOf(e.getKey()), e.getValue().stripTrailingZeros().toPlainString()); |
| | | } |
| | | root.set("lines", linesNode); |
| | | return SCAN_SHIP_REMARK_PREFIX + om.writeValueAsString(root); |
| | | } catch (Exception e) { |
| | | throw new ServiceException("构建发货审批参数失败"); |
| | | } |
| | | } |
| | | |
| | | private ScanShipPayload parseScanShipPayload(String remark) { |
| | | if (!StringUtils.hasText(remark) || !remark.startsWith(SCAN_SHIP_REMARK_PREFIX)) { |
| | | return null; |
| | | } |
| | | try { |
| | | String json = remark.substring(SCAN_SHIP_REMARK_PREFIX.length()); |
| | | ObjectMapper om = new ObjectMapper(); |
| | | JsonNode n = om.readTree(json); |
| | | ScanShipPayload p = new ScanShipPayload(); |
| | | p.shippingNo = n.path("shippingNo").asText(null); |
| | | p.ledgerId = n.path("ledgerId").asLong(0L); |
| | | p.car = n.path("car").asText(""); |
| | | p.express = n.path("express").asText(""); |
| | | p.shipType = n.path("shipType").asText("货车"); |
| | | JsonNode lines = n.path("lines"); |
| | | p.linesQty = new LinkedHashMap<>(); |
| | | if (lines.isObject()) { |
| | | Iterator<String> it = lines.fieldNames(); |
| | | while (it.hasNext()) { |
| | | String k = it.next(); |
| | | p.linesQty.put(Long.valueOf(k), new BigDecimal(lines.get(k).asText())); |
| | | } |
| | | } |
| | | return p; |
| | | } catch (Exception e) { |
| | | log.warn("解析扫码发货审批备注失败: {}", e.getMessage()); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void onScanShipDeliveryApproveOutcome(ApproveProcess approveProcess, Integer outcomeStatus) { |
| | | if (approveProcess == null) { |
| | | return; |
| | | } |
| | | ScanShipPayload ctx = parseScanShipPayload(approveProcess.getApproveRemark()); |
| | | if (ctx == null || ctx.ledgerId == null || ctx.ledgerId <= 0 || !StringUtils.hasText(ctx.shippingNo)) { |
| | | return; |
| | | } |
| | | if (outcomeStatus != null && outcomeStatus == 2) { |
| | | executeScanShipDeliveryApproved(approveProcess, ctx); |
| | | } else if (outcomeStatus != null && outcomeStatus == 3) { |
| | | updateScanShipBatchShippingStatus(ctx.ledgerId, ctx.shippingNo, "审核拒绝"); |
| | | SalesLedger sl = baseMapper.selectById(ctx.ledgerId); |
| | | if (sl != null) { |
| | | sl.setDeliveryStatus(3); |
| | | baseMapper.updateById(sl); |
| | | } |
| | | } else if (outcomeStatus != null && outcomeStatus == 1) { |
| | | updateScanShipBatchShippingStatus(ctx.ledgerId, ctx.shippingNo, "审核中"); |
| | | } |
| | | } |
| | | |
| | | private void updateScanShipBatchShippingStatus(Long ledgerId, String shippingNo, String statusText) { |
| | | if (ledgerId == null || !StringUtils.hasText(shippingNo)) { |
| | | return; |
| | | } |
| | | shippingInfoMapper.update(null, new UpdateWrapper<ShippingInfo>().lambda() |
| | | .set(ShippingInfo::getStatus, statusText) |
| | | .eq(ShippingInfo::getSalesLedgerId, ledgerId) |
| | | .eq(ShippingInfo::getShippingNo, shippingNo)); |
| | | } |
| | | |
| | | private void executeScanShipDeliveryApproved(ApproveProcess approveProcess, ScanShipPayload ctx) { |
| | | int saleType = SaleEnum.SALE.getCode(); |
| | | Date now = new Date(); |
| | | List<ShippingInfo> batch = shippingInfoMapper.selectList(Wrappers.<ShippingInfo>lambdaQuery() |
| | | .eq(ShippingInfo::getSalesLedgerId, ctx.ledgerId) |
| | | .eq(ShippingInfo::getShippingNo, ctx.shippingNo)); |
| | | if (CollectionUtils.isEmpty(batch)) { |
| | | log.warn("扫码发货审批通过但未找到发货台账 batch ledgerId={} shippingNo={}", ctx.ledgerId, ctx.shippingNo); |
| | | return; |
| | | } |
| | | for (Map.Entry<Long, BigDecimal> entry : ctx.linesQty.entrySet()) { |
| | | Long productLineId = entry.getKey(); |
| | | BigDecimal shipQty = entry.getValue(); |
| | | if (shipQty == null || shipQty.compareTo(BigDecimal.ZERO) <= 0) { |
| | | continue; |
| | | } |
| | | SalesLedgerProduct dbProduct = salesLedgerProductMapper.selectById(productLineId); |
| | | if (dbProduct == null) { |
| | | throw new ServiceException("销售产品不存在"); |
| | | } |
| | | ShippingInfo row = batch.stream() |
| | | .filter(si -> Objects.equals(si.getSalesLedgerProductId(), productLineId)) |
| | | .findFirst() |
| | | .orElse(null); |
| | | if (row == null) { |
| | | throw new ServiceException("未找到对应发货台账行"); |
| | | } |
| | | if (!"已发货".equals(row.getStatus())) { |
| | | stockUtils.substractStock(ctx.ledgerId, productLineId, dbProduct.getProductModelId(), shipQty, |
| | | StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), row.getId()); |
| | | BigDecimal oldShipped = defaultDecimal(dbProduct.getShippedQuantity()); |
| | | dbProduct.setShippedQuantity(oldShipped.add(shipQty)); |
| | | dbProduct.fillRemainingQuantity(); |
| | | salesLedgerProductMapper.updateById(dbProduct); |
| | | } |
| | | row.setStatus("已发货"); |
| | | row.setShippingDate(now); |
| | | if (StringUtils.hasText(ctx.car)) { |
| | | row.setShippingCarNumber(ctx.car.trim()); |
| | | } |
| | | if (StringUtils.hasText(ctx.express)) { |
| | | row.setExpressNumber(ctx.express.trim()); |
| | | } |
| | | if (StringUtils.hasText(ctx.shipType)) { |
| | | row.setType(ctx.shipType.trim()); |
| | | } |
| | | shippingInfoMapper.updateById(row); |
| | | } |
| | | List<Long> shippingIds = batch.stream().map(ShippingInfo::getId).filter(Objects::nonNull).collect(Collectors.toList()); |
| | | commonFileService.copyApproveProcessShipAttachmentsToShippingInfos(approveProcess.getId(), shippingIds); |
| | | List<SalesLedgerProduct> ledgerAllProducts = salesLedgerProductMapper.selectList( |
| | | Wrappers.<SalesLedgerProduct>lambdaQuery().eq(SalesLedgerProduct::getSalesLedgerId, ctx.ledgerId)); |
| | | SalesLedger salesLedger = baseMapper.selectById(ctx.ledgerId); |
| | | if (salesLedger == null) { |
| | | return; |
| | | } |
| | | boolean anyInbound = ledgerAllProducts.stream().anyMatch(p -> { |
| | | BigDecimal sq = p.getStockedQuantity(); |
| | | return sq != null && sq.compareTo(BigDecimal.ZERO) > 0; |
| | | }); |
| | | boolean allLinesFull = ledgerAllProducts.stream().allMatch(p -> Objects.equals(p.getProductStockStatus(), 2)); |
| | | salesLedger.setStockStatus(allLinesFull ? 2 : (anyInbound ? 1 : 0)); |
| | | List<SalesLedgerProduct> saleLines = ledgerAllProducts.stream() |
| | | .filter(p -> Objects.equals(p.getType(), saleType)) |
| | | .collect(Collectors.toList()); |
| | | boolean allDelivered = !saleLines.isEmpty() && saleLines.stream().allMatch(p -> { |
| | | BigDecimal q = defaultDecimal(p.getQuantity()); |
| | | BigDecimal s = defaultDecimal(p.getShippedQuantity()); |
| | | return q.compareTo(BigDecimal.ZERO) <= 0 || s.compareTo(q) >= 0; |
| | | }); |
| | | if (allDelivered) { |
| | | salesLedger.setDeliveryStatus(5); |
| | | } else { |
| | | boolean anyLineShipped = saleLines.stream() |
| | | .anyMatch(p -> defaultDecimal(p.getShippedQuantity()).compareTo(BigDecimal.ZERO) > 0); |
| | | if (anyLineShipped) { |
| | | salesLedger.setDeliveryStatus(6); |
| | | } |
| | | } |
| | | baseMapper.updateById(salesLedger); |
| | | } |
| | | } |