From 6ef4265f1859e88e3e5ff22ef1848e12fa849e26 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期二, 12 五月 2026 07:03:43 +0800
Subject: [PATCH] feat: 扫码出库修改为扫码发货
---
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java | 40 +-
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java | 7
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java | 303 +++++++++++++++++++++++++
src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java | 30 ++
src/main/java/com/ruoyi/sales/dto/SalesScanShipDto.java | 37 +++
src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java | 13 +
src/test/java/com/ruoyi/sales/InvoiceLedgerReceiptIncomeRebuildBatchTest.java | 264 ++++++++++++++++++++++
7 files changed, 675 insertions(+), 19 deletions(-)
diff --git a/src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java b/src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
index c42eeab..3faa5d3 100644
--- a/src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
+++ b/src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
@@ -259,26 +259,28 @@
}
// 鍑哄簱瀹℃壒淇敼 (璁㈠崟绾у埆)
if(approveProcess.getApproveType().equals(7)){
- String[] split = approveProcess.getApproveReason().split(":");
- if (split.length > 1) {
- String identifier = split[1];
- // 鏌ユ壘閿�鍞彴璐�
- SalesLedger salesLedger = salesLedgerMapper.selectOne(new LambdaQueryWrapper<SalesLedger>()
- .eq(SalesLedger::getSalesContractNo, identifier)
- .last("limit 1"));
+ String scanRemark = approveProcess.getApproveRemark();
+ if (org.springframework.util.StringUtils.hasText(scanRemark) && scanRemark.startsWith("SCAN_SHIP_DELIVERY_JSON:")) {
+ salesLedgerService.onScanShipDeliveryApproveOutcome(approveProcess, status);
+ } else {
+ String[] split = approveProcess.getApproveReason().split(":");
+ if (split.length > 1) {
+ String identifier = split[1];
+ SalesLedger salesLedger = salesLedgerMapper.selectOne(new LambdaQueryWrapper<SalesLedger>()
+ .eq(SalesLedger::getSalesContractNo, identifier)
+ .last("limit 1"));
- if (salesLedger != null) {
- if(status.equals(2)){
- // 瀹℃壒瀹屾垚 -> 淇敼鐘舵�佷负瀹℃牳閫氳繃锛屼笉鎵i櫎搴撳瓨锛堟墸闄ゅ簱瀛樺湪鍙戣揣鍙拌处琛ュ厖淇℃伅锛�
- updateSalesLedgerDeliveryStatus(salesLedger.getId(), 4);
- updateShippingInfoStatusByOrder(salesLedger.getId(), "瀹℃牳閫氳繃");
- } else if(status.equals(3)){
- updateSalesLedgerDeliveryStatus(salesLedger.getId(), 3);
- // 鏇存柊鍏宠仈鐨勫彂璐ц褰曚负瀹℃牳鎷掔粷
- updateShippingInfoStatusByOrder(salesLedger.getId(), "瀹℃牳鎷掔粷");
- } else if(status.equals(1)){
- updateSalesLedgerDeliveryStatus(salesLedger.getId(), 2);
- updateShippingInfoStatusByOrder(salesLedger.getId(), "瀹℃牳涓�");
+ if (salesLedger != null) {
+ if(status.equals(2)){
+ updateSalesLedgerDeliveryStatus(salesLedger.getId(), 4);
+ updateShippingInfoStatusByOrder(salesLedger.getId(), "瀹℃牳閫氳繃");
+ } else if(status.equals(3)){
+ updateSalesLedgerDeliveryStatus(salesLedger.getId(), 3);
+ updateShippingInfoStatusByOrder(salesLedger.getId(), "瀹℃牳鎷掔粷");
+ } else if(status.equals(1)){
+ updateSalesLedgerDeliveryStatus(salesLedger.getId(), 2);
+ updateShippingInfoStatusByOrder(salesLedger.getId(), "瀹℃牳涓�");
+ }
}
}
}
diff --git a/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java b/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
index 26c14a5..0825a8d 100644
--- a/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
+++ b/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -342,6 +342,13 @@
return AjaxResult.success();
}
+ @PostMapping("/scanShipApply")
+ @ApiOperation("閿�鍞鍗曟壂鐮�-鍙戣捣鍙戣揣瀹℃壒锛堝~鍐欒溅鐗�/蹇�掋�佸鎵逛汉銆侀檮浠讹紱閫氳繃鍚庤嚜鍔ㄦ墸搴撳瓨骞舵爣璁板凡鍙戣揣锛�")
+ public AjaxResult scanShipApply(@RequestBody SalesScanShipDto dto) {
+ salesLedgerService.scanShipApply(dto);
+ return AjaxResult.success("鍙戣揣瀹℃壒宸插彂璧�");
+ }
+
@PostMapping("/scanOutboundUnqualified")
@ApiOperation("閿�鍞鍗曟壂鐮�-涓嶅悎鏍煎嚭搴�")
public AjaxResult scanOutboundUnqualified(@RequestBody SalesScanInboundDto dto) {
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesScanShipDto.java b/src/main/java/com/ruoyi/sales/dto/SalesScanShipDto.java
new file mode 100644
index 0000000..9c3a2c9
--- /dev/null
+++ b/src/main/java/com/ruoyi/sales/dto/SalesScanShipDto.java
@@ -0,0 +1,37 @@
+package com.ruoyi.sales.dto;
+
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * APP 鎵爜鍙戣揣锛氬~鍐欏彂璐т俊鎭�佸鎵逛汉銆侀檮浠跺悗鍙戣捣鍙戣揣瀹℃壒锛涘鎵归�氳繃鍚庤嚜鍔ㄦ墸搴撳瓨骞舵爣璁板凡鍙戣揣銆�
+ */
+@Data
+@ApiModel(value = "SalesScanShipDto", description = "閿�鍞壂鐮佸彂璐э紙鍙戣捣瀹℃壒锛�")
+public class SalesScanShipDto {
+
+ @ApiModelProperty("閿�鍞鍗� Id")
+ private Long salesLedgerId;
+
+ @ApiModelProperty("鏈鍙戣揣鏁伴噺")
+ private List<SalesLedgerProduct> salesLedgerProductList;
+
+ @ApiModelProperty(value = "瀹℃壒浜� userId锛岄�楀彿鍒嗛殧", required = true)
+ private String approveUserIds;
+
+ @ApiModelProperty("鍙戣揣绫诲瀷锛氳揣杞� / 蹇��")
+ private String shipType;
+
+ @ApiModelProperty("杞︾墝鍙�")
+ private String shippingCarNumber;
+
+ @ApiModelProperty("蹇�掑崟鍙�")
+ private String expressNumber;
+
+ @ApiModelProperty("涓存椂鏂囦欢 id 鍒楄〃")
+ private List<String> tempFileIds;
+}
diff --git a/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java b/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
index 048d8be..ca9de2d 100644
--- a/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
+++ b/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.common.enums.SaleEnum;
+import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.sales.dto.*;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProcessRoute;
@@ -85,6 +86,18 @@
void scanOutboundUnqualified(SalesScanInboundDto dto);
+ /**
+ * APP 鎵爜鍙戣揣锛氬彂璧峰彂璐у鎵癸紙瀹℃壒閫氳繃鍚庤嚜鍔ㄦ墸搴撳瓨銆佸彂璐у彴璐︿笌璁㈠崟鐘舵�佷负宸插彂璐э級
+ */
+ void scanShipApply(SalesScanShipDto dto);
+
+ /**
+ * 鍙戣揣瀹℃壒锛堢被鍨� 7锛夎妭鐐圭姸鎬佸彉鏇达細鎵爜鍙戣揣娴佺▼ {@code approveRemark} 浠� {@code SCAN_SHIP_DELIVERY_JSON:} 寮�澶存椂鍥炶皟銆�
+ *
+ * @param outcomeStatus 瀹℃壒娴佺姸鎬侊細1 瀹℃牳涓� 2 閫氳繃 3 鎷掔粷
+ */
+ void onScanShipDeliveryApproveOutcome(ApproveProcess approveProcess, Integer outcomeStatus);
+
void shippingImport(MultipartFile file);
void notShippingImport(MultipartFile file);
diff --git a/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
index dfc054f..8d93b46 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
@@ -2,6 +2,7 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.ruoyi.common.enums.FileNameType;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.StringUtils;
@@ -30,6 +31,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
+import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@@ -173,6 +175,34 @@
}
}
+ /**
+ * 灏嗗鎵瑰崟涓婄殑闄勪欢璁板綍澶嶅埗涓哄鏉″彂璐у彴璐﹂檮浠讹紙鍚屼竴鏂囦欢璺緞锛屾瘡鏉″彂璐у彴璐﹀悇涓�鏉¤褰曪級銆�
+ */
+ public void copyApproveProcessShipAttachmentsToShippingInfos(Long approveProcessId, List<Long> shippingInfoIds) {
+ if (approveProcessId == null || CollectionUtils.isEmpty(shippingInfoIds)) {
+ return;
+ }
+ List<CommonFile> files = commonFileMapper.selectList(new LambdaQueryWrapper<CommonFile>()
+ .eq(CommonFile::getCommonId, approveProcessId)
+ .eq(CommonFile::getType, FileNameType.ApproveProcess.getValue()));
+ if (CollectionUtils.isEmpty(files)) {
+ return;
+ }
+ List<Long> distinctTargets = shippingInfoIds.stream().filter(java.util.Objects::nonNull).distinct().collect(Collectors.toList());
+ for (Long sid : distinctTargets) {
+ for (CommonFile f : files) {
+ CommonFile copy = new CommonFile();
+ copy.setCommonId(sid);
+ copy.setName(f.getName());
+ copy.setUrl(f.getUrl());
+ copy.setLink(f.getLink());
+ copy.setType(FileNameType.SHIP.getValue());
+ copy.setCreateTime(LocalDateTime.now());
+ commonFileMapper.insert(copy);
+ }
+ }
+ }
+
private String buildAccessLink(Path formalFilePath) {
String normalizedPath = formalFilePath.toString().replace("\\", "/");
String profile = RuoYiConfig.getProfile();
diff --git a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
index ff32a36..61777bb 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -1,6 +1,7 @@
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;
@@ -11,6 +12,9 @@
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;
@@ -3012,4 +3016,303 @@
}
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("瑙f瀽鎵爜鍙戣揣瀹℃壒澶囨敞澶辫触: {}", 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);
+ }
}
diff --git a/src/test/java/com/ruoyi/sales/InvoiceLedgerReceiptIncomeRebuildBatchTest.java b/src/test/java/com/ruoyi/sales/InvoiceLedgerReceiptIncomeRebuildBatchTest.java
new file mode 100644
index 0000000..14f50f2
--- /dev/null
+++ b/src/test/java/com/ruoyi/sales/InvoiceLedgerReceiptIncomeRebuildBatchTest.java
@@ -0,0 +1,264 @@
+package com.ruoyi.sales;
+
+import com.ruoyi.RuoYiApplication;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * <b>涓氬姟鍙e緞</b>锛氬巻鍙查噷 {@code receipt_payment.invoice_ledger_id} 甯歌瀛樻垚 <b>閿�鍞骇鍝佽 id</b>锛坽@code sales_ledger_product_id}锛夛紝
+ * 涓庣湡瀹� {@code invoice_ledger.id} 涓嶄竴鑷淬�傚悓姝ユ椂搴斾互 <b>寮�绁ㄥ彴璐�</b> 涓哄噯锛氬嚒鏄�屽凡缁忓紑绁ㄣ�嶇殑閿�鍞彴璐︿骇鍝佽锛�
+ * 灏卞繀椤绘湁涓庝箣瀵瑰簲鐨勫洖娆炬祦姘翠笌鍥炴鏀跺叆锛屼笖<strong>涓�琛屽紑绁ㄥ彴璐� 鈫� 涓�琛屽洖娆� 鈫� 涓�琛屾敹鍏�</strong>銆�
+ * <p>
+ * <b>鎺ㄨ崘锛歿@link #syncReceiptByInvoicedSalesProducts_hbtmblcSchema()}</b>
+ * <ol>
+ * <li>鏌ュ嚭鎵�鏈夊湪寮�绁ㄥ彴璐﹂噷鍑虹幇杩囩殑 {@code sales_ledger_product_id}锛坽@code invoice_ledger} 鈫� {@code invoice_registration_product}锛夈��</li>
+ * <li>鍒犻櫎杩欎簺<strong>閿�鍞骇鍝佽</strong>涓婄幇鏈夌殑 {@code receipt_payment}锛屽苟鍒犻櫎 {@code business_type=1} 涓旀寕鍦ㄨ繖浜涘洖娆句笂鐨� {@code account_income}銆�</li>
+ * <li>鎸� {@code invoice_ledger} 閫愯鎻掑叆鏂扮殑 {@code receipt_payment}锛歿@code sales_ledger_id}銆亄@code sales_ledger_product_id} 鏉ヨ嚜鐧昏浜у搧琛岋紝
+ * {@code invoice_ledger_id = il.id}锛堢湡瀹炲彴璐︿富閿級锛岄噾棰�/鏃ユ湡/寮�绁ㄤ汉涓庡紑绁ㄥ彴璐︿竴鑷淬��</li>
+ * <li>鎸夋柊鍥炴涓婚敭鎻掑叆 {@code account_income}锛堜笌 {@link com.ruoyi.sales.service.impl.ReceiptPaymentServiceImpl} 涓�鑷达細{@code business_id=receipt_payment.id}锛夈��</li>
+ * </ol>
+ * 鏈湪寮�绁ㄩ噷鍑虹幇鐨勯攢鍞骇鍝侊紝鍏跺洖娆捐褰�<strong>涓嶅垹涓嶆敼</strong>銆�
+ * <p>
+ * <b>鎱庣敤锛歿@link #fullRebuildReceiptFromInvoiceLedger_hbtmblcSchema()}</b> 鈥� 娓呯┖<b>鍏ㄩ儴</b>鍥炴涓�<b>鍏ㄩ儴</b>鍥炴绫绘敹鍏ュ悗閲嶅缓锛�
+ * 浠呯敤浜庢暣搴撲互寮�绁ㄤ负鍞竴浜嬪疄婧愮殑鍦烘櫙銆�
+ * <p>
+ * <b>鍐欏簱寮�鍏�</b>锛歿@code -Druoyi.invoiceReceiptRebuild.commit=true} 鎴� {@code RUOYI_INVOICE_RECEIPT_REBUILD_COMMIT=true}銆�
+ */
+@SpringBootTest(classes = RuoYiApplication.class)
+class InvoiceLedgerReceiptIncomeRebuildBatchTest {
+
+ private static final Logger log = LoggerFactory.getLogger(InvoiceLedgerReceiptIncomeRebuildBatchTest.class);
+
+ private static final Long TENANT_ID = null;
+
+ private static final String DEFAULT_REGISTRANT = "妯婂織鑻�";
+
+ private static final String RECEIPT_PAYMENT_TYPE = "0";
+
+ static boolean commitSwitchEnabled() {
+ String p = System.getProperty("ruoyi.invoiceReceiptRebuild.commit");
+ if (p != null && "true".equalsIgnoreCase(p.trim())) {
+ return true;
+ }
+ String e = System.getenv("RUOYI_INVOICE_RECEIPT_REBUILD_COMMIT");
+ return e != null && "true".equalsIgnoreCase(e.trim());
+ }
+
+ static MapSqlParameterSource tenantParams() {
+ return new MapSqlParameterSource("tenantId", TENANT_ID);
+ }
+
+ static String tenantCondIl() {
+ return "(:tenantId IS NULL OR COALESCE(irp.tenant_id, il.tenant_id) = :tenantId)";
+ }
+
+ static String tenantRp(String alias) {
+ return "(:tenantId IS NULL OR " + alias + ".tenant_id = :tenantId)";
+ }
+
+ /**
+ * 鍑虹幇鍦ㄥ紑绁ㄩ摼璺噷鐨勯攢鍞骇鍝� id 闆嗗悎锛堝瓙鏌ヨ鐗囨锛屼笉鍚灞傛嫭鍙凤級銆�
+ */
+ static String invoicedProductIdSubquery() {
+ return "SELECT DISTINCT irp.sales_ledger_product_id "
+ + "FROM invoice_ledger il "
+ + "INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id "
+ + "WHERE irp.sales_ledger_product_id IS NOT NULL "
+ + "AND (" + tenantCondIl() + ")";
+ }
+
+ @Autowired
+ private DataSource dataSource;
+
+ /**
+ * 鎸夈�屽凡寮�绁ㄧ殑閿�鍞彴璐︿骇鍝佽銆嶉噸寤哄洖娆句笌鏀跺叆锛堟帹鑽愶級銆�
+ */
+ @Test
+ @Transactional
+ void syncReceiptByInvoicedSalesProducts_hbtmblcSchema() {
+ NamedParameterJdbcTemplate named = new NamedParameterJdbcTemplate(dataSource);
+ MapSqlParameterSource p = tenantParams();
+
+ String deleteIncome =
+ "DELETE ai FROM account_income ai "
+ + "INNER JOIN receipt_payment rp ON ai.business_id = rp.id AND ai.business_type = 1 "
+ + "WHERE rp.sales_ledger_product_id IN (" + invoicedProductIdSubquery() + ") "
+ + "AND (" + tenantRp("rp") + ")";
+ int delInc = named.update(deleteIncome, p);
+ log.warn("宸插垹闄ゃ�屽凡寮�绁ㄤ骇鍝併�嶅叧鑱旂殑鍥炴鏀跺叆 account_income 琛屾暟: {}", delInc);
+
+ String deleteReceipt =
+ "DELETE rp FROM receipt_payment rp "
+ + "WHERE rp.sales_ledger_product_id IN (" + invoicedProductIdSubquery() + ") "
+ + "AND (" + tenantRp("rp") + ")";
+ int delRp = named.update(deleteReceipt, p);
+ log.warn("宸插垹闄ゃ�屽凡寮�绁ㄤ骇鍝併�嶄笂鐨勬棫鍥炴 receipt_payment 琛屾暟: {}", delRp);
+
+ MapSqlParameterSource insRp = tenantParams().addValue("rpType", RECEIPT_PAYMENT_TYPE);
+ String insertReceipt =
+ "INSERT INTO receipt_payment ("
+ + "sales_ledger_id, sales_ledger_product_id, invoice_ledger_id, "
+ + "receipt_payment_type, receipt_payment_amount, registrant, receipt_payment_date, "
+ + "create_time, create_user, update_time, update_user, tenant_id) "
+ + "SELECT "
+ + " CAST(irp.sales_ledger_id AS SIGNED), "
+ + " CAST(irp.sales_ledger_product_id AS SIGNED), "
+ + " il.id, "
+ + " :rpType, "
+ + " il.invoice_total, "
+ + " NULLIF(TRIM(il.invoice_person), ''), "
+ + " DATE(il.invoice_date), "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " il.create_user, "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " il.update_user, "
+ + " COALESCE(irp.tenant_id, il.tenant_id) "
+ + "FROM invoice_ledger il "
+ + "INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id "
+ + "WHERE irp.sales_ledger_product_id IS NOT NULL "
+ + "AND (" + tenantCondIl() + ")";
+
+ int insRpCnt = named.update(insertReceipt, insRp);
+ log.warn("宸叉寜寮�绁ㄥ彴璐︽彃鍏� receipt_payment 琛屾暟: {}", insRpCnt);
+
+ MapSqlParameterSource regP = tenantParams().addValue("defReg", DEFAULT_REGISTRANT);
+ named.update(
+ "UPDATE receipt_payment rp SET rp.registrant = :defReg "
+ + "WHERE (rp.registrant IS NULL OR rp.registrant = '') AND (" + tenantRp("rp") + ")",
+ regP);
+
+ MapSqlParameterSource insAi = tenantParams()
+ .addValue("rpType", RECEIPT_PAYMENT_TYPE)
+ .addValue("defReg", DEFAULT_REGISTRANT);
+ String insertIncome =
+ "INSERT INTO account_income ("
+ + "income_date, income_type, customer_name, income_money, income_described, income_method, "
+ + "invoice_number, input_user, input_time, business_id, business_type, tenant_id, "
+ + "create_time, create_user, update_time, update_user) "
+ + "SELECT "
+ + " DATE(il.invoice_date), "
+ + " '3', "
+ + " sl.customer_name, "
+ + " rp.receipt_payment_amount, "
+ + " '鍥炴鏀跺叆', "
+ + " :rpType, "
+ + " il.invoice_no, "
+ + " COALESCE(NULLIF(TRIM(rp.registrant), ''), :defReg), "
+ + " DATE(il.invoice_date), "
+ + " rp.id, "
+ + " 1, "
+ + " rp.tenant_id, "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " rp.create_user, "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " rp.update_user "
+ + "FROM receipt_payment rp "
+ + "INNER JOIN invoice_ledger il ON il.id = rp.invoice_ledger_id "
+ + "INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id "
+ + "INNER JOIN sales_ledger sl ON sl.id = irp.sales_ledger_id "
+ + "WHERE (" + tenantCondIl() + ") "
+ + "AND (" + tenantRp("rp") + ") "
+ + "AND NOT EXISTS ("
+ + " SELECT 1 FROM account_income x WHERE x.business_id = rp.id AND x.business_type = 1"
+ + ")";
+
+ int insAiCnt = named.update(insertIncome, insAi);
+ log.warn("宸叉彃鍏ュ洖娆炬敹鍏� account_income 琛屾暟: {}", insAiCnt);
+
+ assertTrue(insRpCnt >= 0 && insAiCnt >= 0);
+ }
+
+ /**
+ * 娓呯┖<b>鍏ㄩ儴</b>鍥炴涓庡洖娆剧被鏀跺叆鍚庯紝浠呮寜寮�绁ㄥ彴璐﹂噸寤猴紙浼氬奖鍝嶆湭寮�绁ㄤ骇鍝佺殑鍥炴锛屾厧鐢級銆�
+ */
+ @Test
+ @Transactional
+ @Rollback(false)
+ void fullRebuildReceiptFromInvoiceLedger_hbtmblcSchema() {
+
+ NamedParameterJdbcTemplate named = new NamedParameterJdbcTemplate(dataSource);
+ MapSqlParameterSource insertParams = tenantParams().addValue("rpType", RECEIPT_PAYMENT_TYPE);
+
+ int deletedIncome = named.update("DELETE FROM account_income WHERE business_type = 1", new MapSqlParameterSource());
+ log.warn("fullRebuild: 宸插垹闄ゅ叏閮ㄥ洖娆剧被鏀跺叆琛屾暟: {}", deletedIncome);
+
+ int deletedReceipt = named.update("DELETE FROM receipt_payment", new MapSqlParameterSource());
+ log.warn("fullRebuild: 宸叉竻绌� receipt_payment 琛屾暟: {}", deletedReceipt);
+
+ String insertReceipt =
+ "INSERT INTO receipt_payment ("
+ + "sales_ledger_id, sales_ledger_product_id, invoice_ledger_id, "
+ + "receipt_payment_type, receipt_payment_amount, registrant, receipt_payment_date, "
+ + "create_time, create_user, update_time, update_user, tenant_id) "
+ + "SELECT "
+ + " CAST(irp.sales_ledger_id AS SIGNED), "
+ + " CAST(irp.sales_ledger_product_id AS SIGNED), "
+ + " il.id, "
+ + " :rpType, "
+ + " il.invoice_total, "
+ + " NULLIF(TRIM(il.invoice_person), ''), "
+ + " DATE(il.invoice_date), "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " il.create_user, "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " il.update_user, "
+ + " COALESCE(irp.tenant_id, il.tenant_id) "
+ + "FROM invoice_ledger il "
+ + "INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id "
+ + "WHERE " + tenantCondIl();
+
+ int insertedReceipt = named.update(insertReceipt, insertParams);
+ log.warn("fullRebuild: 宸叉彃鍏� receipt_payment 琛屾暟: {}", insertedReceipt);
+
+ named.update(
+ "UPDATE receipt_payment SET registrant = :defReg WHERE registrant IS NULL OR registrant = ''",
+ new MapSqlParameterSource("defReg", DEFAULT_REGISTRANT));
+
+ insertParams.addValue("defReg", DEFAULT_REGISTRANT);
+ String insertIncome =
+ "INSERT INTO account_income ("
+ + "income_date, income_type, customer_name, income_money, income_described, income_method, "
+ + "invoice_number, input_user, input_time, business_id, business_type, tenant_id, "
+ + "create_time, create_user, update_time, update_user) "
+ + "SELECT "
+ + " DATE(il.invoice_date), "
+ + " '3', "
+ + " sl.customer_name, "
+ + " rp.receipt_payment_amount, "
+ + " '鍥炴鏀跺叆', "
+ + " :rpType, "
+ + " il.invoice_no, "
+ + " COALESCE(NULLIF(TRIM(rp.registrant), ''), :defReg), "
+ + " DATE(il.invoice_date), "
+ + " rp.id, "
+ + " 1, "
+ + " rp.tenant_id, "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " rp.create_user, "
+ + " CAST(DATE(il.invoice_date) AS DATETIME), "
+ + " rp.update_user "
+ + "FROM receipt_payment rp "
+ + "INNER JOIN invoice_ledger il ON il.id = rp.invoice_ledger_id "
+ + "INNER JOIN invoice_registration_product irp ON irp.id = il.invoice_registration_product_id "
+ + "INNER JOIN sales_ledger sl ON sl.id = irp.sales_ledger_id "
+ + "WHERE " + tenantCondIl();
+
+ int insertedIncome = named.update(insertIncome, insertParams);
+ log.warn("fullRebuild: 宸叉彃鍏� account_income 琛屾暟: {}", insertedIncome);
+
+ assertTrue(insertedReceipt >= 0 && insertedIncome >= 0);
+ }
+}
--
Gitblit v1.9.3