提交
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
-- Region: OVERSEAS
|
||||
-- Schema: baiyue_lastleg
|
||||
-- Purpose: 补齐预约分批/合并后的替代预约业务号快照,供入仓计划查询过滤原预约。
|
||||
-- 可重复执行;不修改既有预约数据。
|
||||
SET @ddl = IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'll_inbound_fba_appointment'
|
||||
AND column_name = 'replacement_nos_json'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_inbound_fba_appointment ADD COLUMN replacement_nos_json LONGTEXT NULL COMMENT ''分批或合并后的新预约业务号列表,非空时原批次只读'' AFTER selected_count'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.lastleg.discard.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
|
||||
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 废弃实物扣减对应的退货余额和普通库存总账。 */
|
||||
@Repository
|
||||
public class CargoDiscardStockLedgerRepository {
|
||||
@Resource private ReturnInventoryBalanceMapper returnBalances;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@Resource private InventoryLedgerMapper ledgers;
|
||||
|
||||
/** 箱内 SKU 扣减时同步扣汇总,只有该 SKU 在箱内耗尽才减一箱。 */
|
||||
public void deductReturn(String customerId, CargoDiscardApplication application, CargoDiscardItem item,
|
||||
long before, long quantity) {
|
||||
var carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !application.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|
||||
|| !"DISCARD_PENDING".equals(carton.getStatus())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货废弃箱归属或状态已变化");
|
||||
}
|
||||
long boxes = before == quantity && !"PARCEL".equals(carton.getSourceUnitType()) ? 1 : 0;
|
||||
var balance = returnBalances.lockSku(customerId, carton.getOverseasWarehouseId(), item.getSkuCode());
|
||||
if (balance == null || returnBalances.deductConversion(customerId, carton.getOverseasWarehouseId(),
|
||||
item.getSkuCode(), quantity, boxes) != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货库存汇总数量不足,不能完成废弃");
|
||||
}
|
||||
}
|
||||
|
||||
/** 一件代发已扣实物仓位后,按同事务余额快照追加库存总账。 */
|
||||
public void recordDropship(String customerId, CargoDiscardApplication application, CargoDiscardItem item,
|
||||
InventoryPosition position, long quantity, String operator, LocalDateTime at) {
|
||||
var balance = balances.selectById(position.getBalanceId());
|
||||
if (balance == null || !customerId.equals(balance.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "废弃库存余额不存在");
|
||||
}
|
||||
var fact = new InventoryLedger();
|
||||
fact.setId(IdWorker.getId()); fact.setCustomerId(customerId); fact.setCustomerName(balance.getCustomerName());
|
||||
fact.setBalanceId(balance.getId()); fact.setPositionId(position.getId());
|
||||
fact.setOverseasWarehouseId(balance.getOverseasWarehouseId()); fact.setDestinationWarehouseCode(balance.getDestinationWarehouseCode());
|
||||
fact.setInboundOrderId(balance.getInboundOrderId()); fact.setInboundCode(balance.getInboundCode());
|
||||
fact.setInboundOrderNo(balance.getInboundOrderNo()); fact.setPalletId(position.getPalletId());
|
||||
fact.setPalletNo(position.getPalletNo()); fact.setBatchNo(position.getBatchNo());
|
||||
fact.setWarehouseLocationCode(position.getWarehouseLocationCode()); fact.setSkuCode(item.getSkuCode());
|
||||
fact.setBusinessType("DISCARD"); fact.setBusinessOrderId(application.getId()); fact.setBusinessOrderNo(application.getDiscardNo());
|
||||
fact.setIdempotencyKey("CARGO_DISCARD:" + application.getId() + ":" + item.getId() + ":STOCK");
|
||||
fact.setStatusFrom("AVAILABLE"); fact.setStatusTo("DISCARDED");
|
||||
fact.setBeforePackages(balance.getAvailablePackages() + quantity); fact.setDeltaPackages(-quantity);
|
||||
fact.setAfterPackages(balance.getAvailablePackages()); fact.setOperatorId(operator);
|
||||
fact.setRemark("货物废弃"); fact.setOccurredAt(at); fact.setCreatedAt(at);
|
||||
if (ledgers.insert(fact) != 1) throw new IllegalStateException("废弃库存流水写入失败");
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -33,6 +33,7 @@ import java.util.Map;
|
||||
/** Final inventory mutation for a warehouse-approved cargo discard. */
|
||||
@Service
|
||||
public class CargoDiscardInventoryAdjustmentService {
|
||||
@Resource private com.baiyue.lastleg.discard.repository.CargoDiscardStockLedgerRepository stockLedger;
|
||||
@Resource private CargoDiscardInventoryMapper inventory;
|
||||
@Resource private CargoDiscardStockService stockService;
|
||||
@Resource private DropshipCartonLedgerMapper cartonLedgers;
|
||||
@@ -44,6 +45,7 @@ public class CargoDiscardInventoryAdjustmentService {
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
|
||||
/** 实物废弃、来源库存扣减与流水在同一事务内完成。 */
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application,
|
||||
CargoDiscardItem item, List<CargoDiscardCarton> links,
|
||||
@@ -53,7 +55,7 @@ public class CargoDiscardInventoryAdjustmentService {
|
||||
stockService.discard(customerId, operatorId, application, item, actualPackages);
|
||||
return new AdjustmentResult(item.getDiscardPackages(), actualPackages, 0);
|
||||
}
|
||||
if (isReturnItem(item)) return discardReturnItem(customerId, item, actualPackages);
|
||||
if (isReturnItem(item)) return discardReturnItem(customerId, application, item, actualPackages);
|
||||
if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量");
|
||||
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
|
||||
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
|
||||
@@ -97,10 +99,12 @@ public class CargoDiscardInventoryAdjustmentService {
|
||||
}
|
||||
long after = value(inventory.skuPackages(customerId, sku.getId()));
|
||||
if (after != before - actualPackages) throw conflict("托盘 SKU 库存扣减不守恒");
|
||||
stockLedger.recordDropship(customerId, application, item, position, actualPackages, operatorId, occurredAt);
|
||||
return new AdjustmentResult(before, actualPackages, after);
|
||||
}
|
||||
|
||||
private AdjustmentResult discardReturnItem(String customerId, CargoDiscardItem item, int actualPackages) {
|
||||
/** 退货箱按件扣减,同时同步库存余额;完成事实由明细的 PDA 完成快照提供。 */
|
||||
private AdjustmentResult discardReturnItem(String customerId, CargoDiscardApplication application, CargoDiscardItem item, int actualPackages) {
|
||||
if (actualPackages < 0 || actualPackages > item.getDiscardPackages()) {
|
||||
throw conflict("实际废弃数量超出退货库存申请数量");
|
||||
}
|
||||
@@ -110,6 +114,7 @@ public class CargoDiscardInventoryAdjustmentService {
|
||||
if (before < actualPackages || (actualPackages > 0 && returnCartonItems.deduct(sku.getId(), actualPackages) != 1)) {
|
||||
throw conflict("退货库存数量不足");
|
||||
}
|
||||
if (actualPackages > 0) stockLedger.deductReturn(customerId, application, item, before, actualPackages);
|
||||
return new AdjustmentResult(before, actualPackages, before - actualPackages);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -26,7 +26,10 @@ public final class ReturnInventoryResponses {
|
||||
int skuTypes, long totalQuantity, BigDecimal length, BigDecimal width, BigDecimal height,
|
||||
BigDecimal weight, LocalDateTime receivedAt, List<CartonItem> items,
|
||||
String locationCode, String palletCode, LocalDateTime putawayAt, List<MergeHistory> mergeHistory,
|
||||
long relabelledQuantity, long relabelReservedQuantity, List<SkuConversion> skuConversions) { }
|
||||
long relabelledQuantity, long relabelReservedQuantity, List<SkuConversion> skuConversions,
|
||||
List<DiscardHistory> discardHistory) { }
|
||||
public record DiscardHistory(String discardNo, String skuCode, String productNameZh, long beforeQuantity,
|
||||
long quantity, long afterQuantity, String operatorId, LocalDateTime occurredAt) { }
|
||||
public record SkuConversion(String relabelNo, String cartonMark, String oldSkuCode, String newSkuCode,
|
||||
long quantity, String status, LocalDateTime completedAt) {
|
||||
public static SkuConversion from(com.baiyue.lastleg.returnreceipt.entity.ReturnSkuConversion row) {
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.returnreceipt.mapper;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
/** 从不可重复执行的 PDA 完成快照读取退货废弃事实,兼容已完成的历史单据。 */
|
||||
@Mapper
|
||||
public interface ReturnDiscardMovementMapper {
|
||||
String FACTS = " FROM ll_cargo_discard_item i JOIN ll_cargo_discard_application a " +
|
||||
"ON a.id=i.application_id AND a.customer_id=i.customer_id " +
|
||||
"JOIN ll_return_inventory_carton c ON c.id=i.return_carton_id AND c.customer_id=i.customer_id " +
|
||||
"AND c.overseas_warehouse_id=a.overseas_warehouse_id " +
|
||||
"WHERE i.pda_status='COMPLETED' AND i.actual_discard_packages>0 ";
|
||||
String MOVEMENTS = "SELECT i.customer_id customerId,a.overseas_warehouse_id overseasWarehouseId,i.sku_code skuCode,i.product_name productNameZh," +
|
||||
"'DISCARD' movementType,a.discard_no referenceNo,-i.actual_discard_packages totalDelta,-i.actual_discard_packages availableDelta,i.pda_completed_at occurredAt" + FACTS;
|
||||
|
||||
/** 按客户与实物箱读取每次废弃的件数、操作者和操作时间。 */
|
||||
@Select("SELECT a.discard_no discardNo,i.sku_code skuCode,i.product_name productNameZh," +
|
||||
"i.available_packages_snapshot beforeQuantity,i.actual_discard_packages quantity," +
|
||||
"i.available_packages_snapshot-i.actual_discard_packages afterQuantity," +
|
||||
"i.pda_operator_id operatorId,i.pda_completed_at occurredAt" + FACTS +
|
||||
"AND i.customer_id=#{customerId} AND i.return_carton_id=#{cartonId} ORDER BY i.pda_completed_at DESC,i.id DESC")
|
||||
List<ReturnInventoryResponses.DiscardHistory> cartonHistory(@Param("customerId") String customerId, @Param("cartonId") long cartonId);
|
||||
}
|
||||
+2
-1
@@ -22,7 +22,8 @@ public interface ReturnInventoryMovementMapper extends com.baomidou.mybatisplus.
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,old_sku_code,product_name_zh,'SKU_CONVERSION_OUT',relabel_no,-quantity,-quantity,completed_at FROM ll_return_sku_conversion" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_IN',relabel_no,quantity,quantity,completed_at FROM ll_return_sku_conversion" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,old_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_IN',relabel_no,quantity,quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'" +
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_OUT',relabel_no,-quantity,-quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'";
|
||||
" UNION ALL SELECT customer_id,overseas_warehouse_id,new_sku_code,product_name_zh,'SKU_CONVERSION_REVERSE_OUT',relabel_no,-quantity,-quantity,reversed_at FROM ll_return_sku_conversion WHERE status='REVERSED'" +
|
||||
" UNION ALL " + ReturnDiscardMovementMapper.MOVEMENTS;
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM (" + MOVEMENTS + ") m WHERE 1=1 " +
|
||||
"<if test='customerId != null and customerId != \"\"'>AND m.customerId=#{customerId} </if>" +
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.baiyue.lastleg.returnreceipt.repository;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnDiscardMovementMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 退货箱废弃历史的持久化查询入口。 */
|
||||
@Repository
|
||||
public class ReturnDiscardMovementRepository {
|
||||
@Resource private ReturnDiscardMovementMapper movements;
|
||||
|
||||
/** 仅返回指定客户实物箱已经完成的废弃事实。 */
|
||||
public List<ReturnInventoryResponses.DiscardHistory> cartonHistory(String customerId, long cartonId) {
|
||||
return movements.cartonHistory(customerId, cartonId);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -63,6 +63,7 @@ public class ReturnInventoryService {
|
||||
@Resource private ReturnInventoryOperationMapper operations;
|
||||
@Resource private ReturnInventoryQueryMapper queries;
|
||||
@Resource private ReturnInventoryMovementMapper movements;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.repository.ReturnDiscardMovementRepository discardMovements;
|
||||
@Resource private LastlegOrderNoGenerator numbers;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper orders;
|
||||
@Resource private com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderItemMapper orderItems;
|
||||
@@ -1242,6 +1243,7 @@ public class ReturnInventoryService {
|
||||
return "PENDING".equals(carton.getCountStatus()) && AVAILABLE.equals(carton.getStatus()) ? "PENDING_COUNT" : carton.getStatus();
|
||||
}
|
||||
|
||||
/** 组装箱内余量及合箱、换标、废弃操作历史。 */
|
||||
private ReturnInventoryResponses.CartonDetail cartonDetail(ReturnInventoryCarton carton) {
|
||||
List<ReturnInventoryResponses.CartonItem> items = cartonItems.listCarton(carton.getId()).stream()
|
||||
.map(item -> new ReturnInventoryResponses.CartonItem(item.getSkuCode(), item.getProductNameZh(), item.getQuantity(),
|
||||
@@ -1253,7 +1255,8 @@ public class ReturnInventoryService {
|
||||
return new ReturnInventoryResponses.CartonDetail(carton.getCartonMark(), carton.getCustomerId(), carton.getOverseasWarehouseId(), carton.getSourceBatchNo(), carton.getSourceReturnNo(),
|
||||
displayStatus(carton), carton.getFbaCartonMark(), carton.getDestinationWarehouseCode(), carton.getDestinationWarehouseName(), carton.getRelabelNo(), relabelProgressStatus(carton), carton.getSkuTypes() == null ? 0 : carton.getSkuTypes(), value(carton.getTotalQuantity()), carton.getLength(), carton.getWidth(), carton.getHeight(), carton.getWeight(), carton.getCreatedAt(), items, carton.getLocationCode(), carton.getPalletCode(), carton.getPutawayAt(), packingTrace.listCartonHistory(carton.getCustomerId(), carton.getCartonMark()),
|
||||
completedRelabelQuantity(carton), cartons.relabelReservedQuantity(carton.getId()),
|
||||
skuConversions.listCarton(carton.getCustomerId(), carton.getId()).stream().map(ReturnInventoryResponses.SkuConversion::from).toList());
|
||||
skuConversions.listCarton(carton.getCustomerId(), carton.getId()).stream().map(ReturnInventoryResponses.SkuConversion::from).toList(),
|
||||
discardMovements.cartonHistory(carton.getCustomerId(), carton.getId()));
|
||||
}
|
||||
|
||||
private String relabelProgressStatus(ReturnInventoryCarton carton) {
|
||||
|
||||
@@ -2,7 +2,7 @@ openapi: 3.0.3
|
||||
info:
|
||||
title: 退货合箱计划与箱级上架
|
||||
version: 1.0.0
|
||||
description: 包裹可包含多个SKU并拥有独立标签;整包裹/箱可直接出库,部分数量须精确选择来源包裹/箱合箱成新标签箱。托盘仅为可选载体。合箱不新增订单货量。SKU编码换标完工时原子扣旧SKU、加新SKU,总件数守恒;客户确认前保持RELABEL_PENDING占用,退回重做冲回本轮转换。CartonDetail返回skuConversions(换标单号、完工识别码、旧新SKU、实际数量、APPLIED/CONFIRMED/REVERSED状态和时间),items返回当前quantity、availableQuantity、relabelledQuantity、remainingRelabelQuantity;新SKU已换标件数不可二次换标。
|
||||
description: 包裹可包含多个SKU并拥有独立标签;整包裹/箱可直接出库,部分数量须精确选择来源包裹/箱合箱成新标签箱。托盘仅为可选载体。合箱不新增订单货量。SKU编码换标完工时原子扣旧SKU、加新SKU,总件数守恒;客户确认前保持RELABEL_PENDING占用,退回重做冲回本轮转换。CartonDetail返回skuConversions(换标单号、完工识别码、旧新SKU、实际数量、APPLIED/CONFIRMED/REVERSED状态和时间),items返回当前quantity、availableQuantity、relabelledQuantity、remainingRelabelQuantity;新SKU已换标件数不可二次换标。CartonDetail新增discardHistory,包含discardNo、skuCode、productNameZh、beforeQuantity、quantity、afterQuantity、operatorId、occurredAt;数量均为件。退货进销存包含DISCARD业务类型,referenceNo为废弃单号,totalDelta和availableDelta为实际废弃件数的负数,发生时间为PDA完成时间,按客户和实际海外仓隔离;已完成历史单据同样可查询。
|
||||
security: [{ bearerAuth: [] }]
|
||||
paths:
|
||||
/api/platform/v1/last-leg/return-packing-plans:
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.repository.CargoDiscardStockLedgerRepository;
|
||||
import com.baiyue.lastleg.inventory.entity.*;
|
||||
import com.baiyue.lastleg.inventory.mapper.*;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.*;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.*;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 校验按件、按箱扣减与总账事实的数量口径。 */
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CargoDiscardStockLedgerRepositoryTest {
|
||||
@Mock ReturnInventoryBalanceMapper returnBalances;
|
||||
@Mock ReturnInventoryCartonMapper returnCartons;
|
||||
@Mock InventoryBalanceMapper balances;
|
||||
@Mock InventoryLedgerMapper ledgers;
|
||||
CargoDiscardStockLedgerRepository repository;
|
||||
|
||||
/** 装配真实仓储逻辑及受控数据库边界。 */
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
repository = new CargoDiscardStockLedgerRepository();
|
||||
ReflectionTestUtils.setField(repository, "returnBalances", returnBalances);
|
||||
ReflectionTestUtils.setField(repository, "returnCartons", returnCartons);
|
||||
ReflectionTestUtils.setField(repository, "balances", balances);
|
||||
ReflectionTestUtils.setField(repository, "ledgers", ledgers);
|
||||
}
|
||||
|
||||
/** 部分废弃保留箱数,整 SKU 耗尽减箱,包裹不占箱数。 */
|
||||
@ParameterizedTest
|
||||
@CsvSource({"BOX,10,5,0", "BOX,5,5,1", "PARCEL,5,5,0"})
|
||||
void returnBalanceUsesPiecesAndRemainingSku(String unit, long before, long count, long boxes) {
|
||||
var app = new CargoDiscardApplication(); app.setOverseasWarehouseId("W");
|
||||
var item = new CargoDiscardItem(); item.setReturnCartonId(1L); item.setSkuCode("SKU");
|
||||
var carton = new ReturnInventoryCarton(); carton.setCustomerId("C"); carton.setOverseasWarehouseId("W");
|
||||
carton.setStatus("DISCARD_PENDING"); carton.setSourceUnitType(unit);
|
||||
when(returnCartons.lockId(1L)).thenReturn(carton);
|
||||
when(returnBalances.lockSku("C", "W", "SKU")).thenReturn(new ReturnInventoryBalance());
|
||||
when(returnBalances.deductConversion("C", "W", "SKU", count, boxes)).thenReturn(1);
|
||||
repository.deductReturn("C", app, item, before, count);
|
||||
verify(returnBalances).deductConversion("C", "W", "SKU", count, boxes);
|
||||
}
|
||||
|
||||
/** 其他客户的退货箱不能扣当前客户库存。 */
|
||||
@Test
|
||||
void rejectsForeignCartonBeforeBalanceMutation() {
|
||||
var item = new CargoDiscardItem(); item.setReturnCartonId(1L);
|
||||
var carton = new ReturnInventoryCarton(); carton.setCustomerId("OTHER");
|
||||
when(returnCartons.lockId(1L)).thenReturn(carton);
|
||||
assertThrows(ResponseStatusException.class, () -> repository.deductReturn("C", new CargoDiscardApplication(), item, 5, 5));
|
||||
verifyNoInteractions(returnBalances);
|
||||
}
|
||||
|
||||
/** 一件代发混装箱只按实物箱数记总账,并关联废弃业务单。 */
|
||||
@Test
|
||||
void dropshipLedgerRecordsPhysicalBoxesAndBusinessReference() {
|
||||
var app = new CargoDiscardApplication(); app.setId(10L); app.setDiscardNo("CD-1");
|
||||
var item = new CargoDiscardItem(); item.setId(20L); item.setSkuCode("MIXED");
|
||||
var position = new InventoryPosition(); position.setId(30L); position.setBalanceId(40L); position.setPalletNo("P1");
|
||||
var balance = new InventoryBalance(); balance.setId(40L); balance.setCustomerId("C"); balance.setAvailablePackages(9L);
|
||||
when(balances.selectById(40L)).thenReturn(balance); when(ledgers.insert(any(InventoryLedger.class))).thenReturn(1);
|
||||
repository.recordDropship("C", app, item, position, 1, "operator", LocalDateTime.now());
|
||||
var fact = ArgumentCaptor.forClass(InventoryLedger.class); verify(ledgers).insert(fact.capture());
|
||||
assertEquals(10L, fact.getValue().getBeforePackages()); assertEquals(-1L, fact.getValue().getDeltaPackages());
|
||||
assertEquals(9L, fact.getValue().getAfterPackages()); assertEquals("CD-1", fact.getValue().getBusinessOrderNo());
|
||||
assertEquals("DISCARD", fact.getValue().getBusinessType()); assertEquals("P1", fact.getValue().getPalletNo());
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.lastleg.discard;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnDiscardMovementMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryMovementMapper;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.util.UUID;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** 用真实 SQL 验证完成事实、历史兼容及客户箱号隔离。 */
|
||||
class ReturnDiscardMovementMapperTest {
|
||||
/** 历史已完成废弃展示负件数;未完成、其他客户及其他仓记录均排除。 */
|
||||
@Test
|
||||
void completedFactsAreVisibleInMovementsAndCartonHistory() {
|
||||
var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
var db = new JdbcTemplate(source);
|
||||
db.execute("CREATE TABLE ll_cargo_discard_application(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),discard_no VARCHAR(30))");
|
||||
db.execute("CREATE TABLE ll_return_inventory_carton(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20))");
|
||||
db.execute("ALTER TABLE ll_return_inventory_carton ADD carton_mark VARCHAR(30), ADD source_batch_no VARCHAR(30), ADD created_at TIMESTAMP");
|
||||
db.execute("CREATE TABLE ll_return_inventory_carton_item(carton_id BIGINT,sku_code VARCHAR(30),product_name_zh VARCHAR(30),quantity BIGINT)");
|
||||
db.execute("CREATE TABLE ll_return_receipt(id BIGINT,return_order_id BIGINT,receipt_no VARCHAR(30),signed_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE ll_return_order(id BIGINT,customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20))");
|
||||
db.execute("CREATE TABLE ll_return_receipt_item(receipt_id BIGINT,sku_code VARCHAR(30),return_order_item_id BIGINT,received_quantity BIGINT)");
|
||||
db.execute("CREATE TABLE ll_return_order_item(id BIGINT,product_name_zh VARCHAR(30))");
|
||||
db.execute("CREATE TABLE ll_return_inventory_outbound_movement(customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),sku_code VARCHAR(30),product_name_zh VARCHAR(30),outbound_no VARCHAR(30),quantity BIGINT,occurred_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE ll_return_carton_stock_receipt(customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),sku_code VARCHAR(30),product_name_zh VARCHAR(30),receipt_no VARCHAR(30),quantity BIGINT,received_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE ll_return_sku_conversion(customer_id VARCHAR(20),overseas_warehouse_id VARCHAR(20),old_sku_code VARCHAR(30),new_sku_code VARCHAR(30),product_name_zh VARCHAR(30),relabel_no VARCHAR(30),quantity BIGINT,completed_at TIMESTAMP,reversed_at TIMESTAMP,status VARCHAR(30))");
|
||||
db.execute("CREATE TABLE ll_cargo_discard_item(id BIGINT,application_id BIGINT,customer_id VARCHAR(20),return_carton_id BIGINT,sku_code VARCHAR(30),product_name VARCHAR(30),pda_status VARCHAR(20),available_packages_snapshot INT,actual_discard_packages INT,pda_operator_id VARCHAR(20),pda_completed_at TIMESTAMP)");
|
||||
db.update("INSERT INTO ll_cargo_discard_application VALUES(1,'C','W','CD-1'),(2,'OTHER','W','CD-2'),(3,'C','OTHER-W','CD-3')");
|
||||
db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id) VALUES(10,'C','W'),(20,'OTHER','W')");
|
||||
db.update("INSERT INTO ll_cargo_discard_item VALUES(1,1,'C',10,'SKU','货物','COMPLETED',10,5,'OP',CURRENT_TIMESTAMP),(2,1,'C',10,'SKU','货物','PENDING',10,5,NULL,NULL),(3,2,'OTHER',20,'SKU','货物','COMPLETED',5,5,'OP',CURRENT_TIMESTAMP),(4,3,'C',10,'SKU','货物','COMPLETED',5,5,'OP',CURRENT_TIMESTAMP)");
|
||||
var config = new Configuration(new Environment("test", new JdbcTransactionFactory(), source));
|
||||
config.addMapper(ReturnDiscardMovementMapper.class);
|
||||
config.addMapper(ReturnInventoryMovementMapper.class);
|
||||
try (var session = new SqlSessionFactoryBuilder().build(config).openSession()) {
|
||||
var mapper = session.getMapper(ReturnDiscardMovementMapper.class);
|
||||
var history = mapper.cartonHistory("C", 10L);
|
||||
assertEquals(1, history.size()); assertEquals("CD-1", history.get(0).discardNo());
|
||||
assertEquals(5, history.get(0).quantity()); assertEquals(5, history.get(0).afterQuantity());
|
||||
assertTrue(mapper.cartonHistory("OTHER", 10L).isEmpty());
|
||||
assertEquals(2, db.queryForObject("SELECT COUNT(*) FROM (" + ReturnDiscardMovementMapper.MOVEMENTS + ") m", Integer.class));
|
||||
var inventory = session.getMapper(ReturnInventoryMovementMapper.class);
|
||||
assertEquals(1, inventory.platformCount("C", "W", "SKU"));
|
||||
var movement = inventory.platformPage("C", "W", "SKU", 0L, 20L).get(0);
|
||||
assertEquals("DISCARD", movement.movementType()); assertEquals("CD-1", movement.referenceNo());
|
||||
assertEquals(-5, movement.totalDelta()); assertEquals(-5, movement.availableDelta());
|
||||
assertEquals(0, inventory.platformCount("C", "W", "MISSING-SKU"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -50,11 +50,15 @@ class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
@Mock DropshipOrderProfileMapper profiles;
|
||||
@Mock InventoryPositionMapper positions;
|
||||
@Mock InventoryBalanceMapper balances;
|
||||
@Mock com.baiyue.lastleg.discard.repository.CargoDiscardStockLedgerRepository stockLedger;
|
||||
@Mock com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper returnItems;
|
||||
private CargoDiscardInventoryAdjustmentService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CargoDiscardInventoryAdjustmentService();
|
||||
ReflectionTestUtils.setField(service, "stockLedger", stockLedger);
|
||||
ReflectionTestUtils.setField(service, "returnCartonItems", returnItems);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "cartonLedgers", ledgers);
|
||||
ReflectionTestUtils.setField(service, "cartonSkus", cartonSkus);
|
||||
@@ -64,6 +68,21 @@ class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
ReflectionTestUtils.setField(service, "balances", balances);
|
||||
}
|
||||
|
||||
/** 退货来源按 SKU 件数同时扣箱明细和客户库存汇总。 */
|
||||
@Test
|
||||
void returnDiscardSynchronizesCartonAndBalance() {
|
||||
var app = application(1);
|
||||
var line = item(5); line.setSourceType("RETURN_CARTON"); line.setReturnCartonId(31L);
|
||||
var content = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem();
|
||||
content.setId(32L); content.setCustomerId("100"); content.setQuantity(5L);
|
||||
when(returnItems.lockSku(31L, "SKU-1")).thenReturn(content);
|
||||
when(returnItems.deduct(32L, 5)).thenReturn(1);
|
||||
var result = service.discardPalletItem("100", app, line, List.of(), 5, "OP", LocalDateTime.now());
|
||||
assertEquals(5, result.discardedPackages()); assertEquals(0, result.afterPackages());
|
||||
verify(stockLedger).deductReturn("100", app, line, 5L, 5L);
|
||||
org.mockito.Mockito.verifyNoInteractions(positions, balances, ledgers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaDiscardDeductsEverySkuInsideOnePhysicalCartonOnce() {
|
||||
CargoDiscardApplication application = application(1);
|
||||
@@ -96,6 +115,7 @@ class CargoDiscardInventoryAdjustmentServiceTest {
|
||||
verify(skuInventories).deduct("100", 201L, "SKU-A", 1, 2);
|
||||
verify(skuInventories).deduct("100", 201L, "SKU-B", 1, 3);
|
||||
verify(positions).deductAvailable("100", 801L, 1);
|
||||
verify(stockLedger).recordDropship(eq("100"), eq(application), eq(item), eq(position), eq(1L), eq("operator"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ class ReturnInventoryServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ReturnInventoryService();
|
||||
ReflectionTestUtils.setField(service, "discardMovements", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.repository.ReturnDiscardMovementRepository.class));
|
||||
ReflectionTestUtils.setField(service, "skuConversions", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnSkuConversionMapper.class));
|
||||
ReflectionTestUtils.setField(service, "cartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "cartonItems", cartonItems);
|
||||
|
||||
@@ -2161,6 +2161,8 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
||||
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_fba_appointment' AND column_name='selected_count' AND COALESCE(column_comment,'') <> '已选明细数量'), 'ALTER TABLE ll_inbound_fba_appointment MODIFY COLUMN selected_count INT NOT NULL DEFAULT 0 COMMENT ''已选明细数量''', 'SELECT 1'));
|
||||
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
|
||||
SET @ddl = (SELECT IF(NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_fba_appointment' AND column_name='replacement_nos_json'), 'ALTER TABLE ll_inbound_fba_appointment ADD COLUMN replacement_nos_json LONGTEXT NULL COMMENT ''分批或合并后的新预约业务号列表,非空时原批次只读'' AFTER selected_count', 'SELECT 1'));
|
||||
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_fba_appointment' AND column_name='created_by' AND COALESCE(column_comment,'') <> '创建人ID'), 'ALTER TABLE ll_inbound_fba_appointment MODIFY COLUMN created_by VARCHAR(128) NULL COMMENT ''创建人ID''', 'SELECT 1'));
|
||||
PREPARE schema_comments_025 FROM @ddl; EXECUTE schema_comments_025; DEALLOCATE PREPARE schema_comments_025;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_fba_appointment' AND column_name='created_at' AND COALESCE(column_comment,'') <> '创建时间'), 'ALTER TABLE ll_inbound_fba_appointment MODIFY COLUMN created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT ''创建时间''', 'SELECT 1'));
|
||||
|
||||
Reference in New Issue
Block a user