Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
+1
-1
@@ -227,7 +227,7 @@ public interface DispatchCandidateMapper {
|
||||
|
||||
@Select("<script>" +
|
||||
"SELECT NULL AS planId,NULL AS planPalletId,pos.pallet_id AS warehousePalletId,a.lastleg_order_id AS orderId," +
|
||||
"a.customer_id AS customerId,NULL AS customerName,NULL AS planNo,'RELABEL_APPLICATION' AS orderType,NULL AS volumeWeightKg,NULL AS grossWeightKg,NULL AS containerNo," +
|
||||
"a.customer_id AS customerId,NULL AS customerName,NULL AS planNo,'RELABEL_APPLICATION' AS orderType,NULL AS volumeWeightKg,MAX(wtp.gross_weight_kg) AS grossWeightKg,NULL AS containerNo," +
|
||||
"NULL AS appointmentAt,NULL AS expectedLoadingAt,a.overseas_warehouse_id AS overseasWarehouseId,MIN(ri.source_warehouse_code) AS overseasWarehouseCode,MIN(ri.source_warehouse_name) AS overseasWarehouseName," +
|
||||
"MIN(COALESCE(NULLIF(ri.source_inbound_order_no,''),NULLIF(a.customer_order_no,''),a.relabel_no)) AS orderNo,a.relabel_no AS orderRefs,MIN(ri.source_inbound_code) AS inboundCode,pos.pallet_no AS palletNo,NULL AS destinationWarehouseId," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode," +
|
||||
|
||||
+19
-2
@@ -79,6 +79,22 @@ public class DispatchService {
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard;
|
||||
@Resource private ReturnRelabelDispatchStock returnRelabelStock;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService relabelMeasurements;
|
||||
|
||||
private List<DispatchPalletCandidate> measuredRelabelCandidates(List<DispatchPalletCandidate> rows) {
|
||||
var cache = new java.util.HashMap<String, List<com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService.Measurement>>();
|
||||
for (var row : rows) {
|
||||
var values = cache.computeIfAbsent(row.customerId() + ":" + row.orderRefs(), ignored -> {
|
||||
var app = relabelApplications.findAnyByRelabelNo(row.orderRefs());
|
||||
return app != null && Objects.equals(app.getCustomerId(), row.customerId()) ? relabelMeasurements.snapshots(app) : List.of();
|
||||
});
|
||||
if (!values.isEmpty()) {
|
||||
var p = values.stream().filter(value -> Objects.equals(value.palletId(), row.warehousePalletId())).findFirst().orElse(null);
|
||||
row.grossWeightKg(p == null ? null : p.grossWeightKg()); row.volumeCbm(p == null ? null : p.volumeCbm());
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
public String resolveCustomerScopeForPlans(List<Long> planIds) {
|
||||
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
|
||||
@@ -124,7 +140,7 @@ public class DispatchService {
|
||||
trimToNull(sourceStage), trimToNull(draftToken));
|
||||
if (!StringUtils.hasText(sourceStage) || Set.of("RECEIVED", "RELABEL").contains(sourceStage)) {
|
||||
List<DispatchPalletCandidate> relabel = java.util.stream.Stream.concat(
|
||||
candidates.relabelAvailable(null, trimToNull(orderNo), null).stream(),
|
||||
measuredRelabelCandidates(candidates.relabelAvailable(null, trimToNull(orderNo), null)).stream(),
|
||||
returnRelabelStock.candidates(null, trimToNull(orderNo), null).stream()).toList();
|
||||
if (relabel != null && !relabel.isEmpty()) {
|
||||
result = java.util.stream.Stream.concat(result.stream(), relabel.stream().filter(item ->
|
||||
@@ -575,10 +591,11 @@ public class DispatchService {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车");
|
||||
}
|
||||
relabelBindingGuard.requireBound(application);
|
||||
relabelMeasurements.requireMeasured(application);
|
||||
if (!"TRUCK".equals(application.getDeliveryMethod()) || !"WAIT_PRE_DISPATCH".equals(application.getFulfillmentStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标单不是待排车状态");
|
||||
List<DispatchPalletCandidate> selected = java.util.stream.Stream.concat(
|
||||
candidates.relabelAvailable(customerId, null, request.relabelNo()).stream(),
|
||||
measuredRelabelCandidates(candidates.relabelAvailable(customerId, null, request.relabelNo())).stream(),
|
||||
returnRelabelStock.candidates(customerId, null, request.relabelNo()).stream()).toList();
|
||||
Set<Long> requested = request.planPalletIds().stream().map(Long::valueOf).collect(Collectors.toSet());
|
||||
if (selected.stream().anyMatch(DispatchPalletCandidate::returnCartonSource)
|
||||
|
||||
+14
@@ -20,6 +20,20 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public class PdaPalletOperationController {
|
||||
@Resource private PdaPalletOperationService service;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService measurements;
|
||||
|
||||
@PostMapping("/relabel-pallet-measurements")
|
||||
public ApiResponse<java.util.List<com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService.Measurement>> measurements(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid @RequestBody PalletOperationRequests.RelabelSources request) {
|
||||
return ApiResponse.success(measurements.list(PdaWarehouseIdentity.require(user), request.relabelNo()));
|
||||
}
|
||||
|
||||
@PostMapping("/relabel-pallet-measure")
|
||||
public ApiResponse<java.util.List<com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService.Measurement>> measure(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService.Measure request) {
|
||||
return ApiResponse.success(measurements.save(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/pallets/resolve")
|
||||
public ApiResponse<PalletOperationResponses.PalletContent> resolve(
|
||||
|
||||
+39
-3
@@ -233,7 +233,6 @@ public class PdaPalletOperationService {
|
||||
List.of(request.target())).get(code(request.target().palletCode()));
|
||||
PalletPhysicalGateway.PalletReference source = request.source() == null ? null
|
||||
: resolvePallets(identity.overseasWarehouseId(), List.of(request.source())).get(code(request.source().palletCode()));
|
||||
if (source != null && source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同");
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = new LinkedHashMap<>();
|
||||
if (source != null) pallets.put(source.palletCode(), source);
|
||||
pallets.put(target.palletCode(), target);
|
||||
@@ -269,7 +268,7 @@ public class PdaPalletOperationService {
|
||||
throw conflict("实际来源托盘库存已变化,请刷新后确认");
|
||||
if (actual != null && application.getOutboundOrderNo() != null && !application.getOutboundOrderNo().isBlank())
|
||||
throw conflict("已生成出库单,请先处理出库单,不能重新分配冻结来源");
|
||||
if (actual != null && marks.stream().anyMatch(boundRelabelMarks(application)::contains))
|
||||
if (actual != null && source.palletId() != target.palletId() && marks.stream().anyMatch(boundRelabelMarks(application)::contains))
|
||||
throw conflict("本批包含本轮已组托的换标箱,请刷新后选择未组托的箱");
|
||||
if ("PROCESSING".equals(application.getStatus())) {
|
||||
var workTask = relabelWorkTasks.find(application.getCustomerId(), application.getId());
|
||||
@@ -283,8 +282,43 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
}
|
||||
if (returnRelabel) {
|
||||
if (source != null && source.palletId() == target.palletId()) throw conflict("退货换标请使用独立目标托盘");
|
||||
return bindReturnRelabelPallet(identity, application, selected, source, target, request, unboundReturnRelabel);
|
||||
}
|
||||
if (source.palletId() == target.palletId()) {
|
||||
return transaction.execute(status -> {
|
||||
var lockedApp = relabelApplications.lockAnyByRelabelNo(application.getRelabelNo());
|
||||
requireRelabelWork(identity, lockedApp);
|
||||
long total = mapper.palletPackages(String.valueOf(identity.overseasWarehouseId()), source.palletId());
|
||||
long selectedPackages = selected.stream().mapToLong(i -> value(i.getNotifiedPackageCount())).sum();
|
||||
if (total <= 0 || total != selectedPackages) throw conflict("部分贴标必须拆到新托盘;只有整托全部贴标可保留原托盘");
|
||||
Map<Long, List<RelabelItem>> byPosition = selected.stream().collect(Collectors.groupingBy(RelabelItem::getSourceInventoryPositionId));
|
||||
Map<Long, InventoryPosition> retained = new LinkedHashMap<>();
|
||||
for (var entry : byPosition.entrySet()) {
|
||||
var row = mapper.lockPosition(application.getOverseasWarehouseId(), entry.getKey());
|
||||
long count = entry.getValue().stream().mapToLong(i -> value(i.getNotifiedPackageCount())).sum();
|
||||
if (row == null || !application.getCustomerId().equals(row.getCustomerId())
|
||||
|| !Objects.equals(row.getPalletId(), source.palletId()) || value(row.getHoldPackages()) < count)
|
||||
throw conflict("原托盘换标库存已变化,请刷新后核对");
|
||||
requirePalletReservation(application.getCustomerId(), entry.getKey(), inventoryCommands.relabelTransfer(application.getCustomerId(), application.getId()));
|
||||
retained.put(entry.getKey(), positions.selectById(entry.getKey()));
|
||||
}
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
|
||||
Set<String> bound = boundRelabelMarks(application);
|
||||
if (!marks.stream().allMatch(bound::contains)) {
|
||||
if (marks.stream().anyMatch(bound::contains)) throw conflict("本批包含已绑定箱唛,请刷新后重试");
|
||||
recordRelabelBinding(identity, new RelabelMutation(application, byPosition, Map.of()), retained);
|
||||
}
|
||||
var result = new PalletOperationResponses.Operation(IdWorker.getId(), "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(),
|
||||
"COMPLETED", false, LocalDateTime.now(), List.of(content(identity.overseasWarehouseId(), source)));
|
||||
var operation = new PdaPalletOperation(); operation.setId(result.operationId());
|
||||
operation.setOverseasWarehouseId(identity.overseasWarehouseId()); operation.setOperatorKey(identity.operatorKey());
|
||||
operation.setOperationType(result.operationType()); operation.setIdempotencyKey(request.idempotencyKey());
|
||||
operation.setRequestHash(hash(request)); operation.setStatus("COMPLETED"); operation.setResultJson(write(result));
|
||||
operation.setCreatedAt(result.completedAt()); operation.setUpdatedAt(result.completedAt()); mapper.insert(operation);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
for (RelabelItem item : selected) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
|
||||
|| item.getNotifiedPackageCount() <= 0) throw conflict("换标箱唛缺少库存位置或箱数快照");
|
||||
@@ -808,7 +842,9 @@ public class PdaPalletOperationService {
|
||||
record.setCreatedAt(LocalDateTime.now());
|
||||
record.setDetailJson(write(Map.of("round", Objects.toString(task.getReviewedAt(), "INITIAL"),
|
||||
"cartonMarks", mutation.itemsByPosition().values().stream().flatMap(List::stream).map(RelabelItem::getCartonMark).toList(),
|
||||
"targetPallets", targets.values().stream().map(InventoryPosition::getPalletNo).distinct().toList())));
|
||||
"targetPallets", targets.values().stream().map(InventoryPosition::getPalletNo).distinct().toList(),
|
||||
"sourcePallets", mutation.itemsByPosition().keySet().stream().map(positions::selectById)
|
||||
.filter(Objects::nonNull).map(InventoryPosition::getPalletNo).distinct().toList())));
|
||||
relabelWorkHistory.insert(record);
|
||||
}
|
||||
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.relabel.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface RelabelPalletMeasurementMapper {
|
||||
record Stock(Long id, Long palletId, String palletNo, long packages, long version) {}
|
||||
record Original(Long palletId, String palletNo, int packages, BigDecimal lengthCm,
|
||||
BigDecimal widthCm, BigDecimal heightCm, BigDecimal grossWeightKg, BigDecimal volumeCbm) {}
|
||||
@Select("SELECT id FROM ll_warehouse_transfer_order WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo}")
|
||||
Long transfer(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
@Select("SELECT id,pallet_id palletId,pallet_no palletNo," +
|
||||
"COALESCE(available_packages,0)+COALESCE(hold_packages,0)+COALESCE(allocated_packages,0)+COALESCE(picked_packages,0)+COALESCE(receiving_packages,0) packages,version " +
|
||||
"FROM ll_inventory_position WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} ORDER BY id")
|
||||
List<Stock> stock(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
|
||||
@Select("SELECT id FROM ll_inventory_position WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND pallet_no=#{palletCode} ORDER BY id FOR UPDATE")
|
||||
List<Long> lockStock(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId, @Param("palletCode") String palletCode);
|
||||
@Select("SELECT warehouse_pallet_id palletId,pallet_no palletNo,carton_count packages,length_cm lengthCm,width_cm widthCm,height_cm heightCm,gross_weight_kg grossWeightKg,volume_cbm volumeCbm " +
|
||||
"FROM ll_warehouse_transfer_pallet WHERE customer_id=#{customerId} AND transfer_order_id=#{transferId} AND status='COMPLETED'")
|
||||
List<Original> originals(@Param("customerId") String customerId, @Param("transferId") Long transferId);
|
||||
@Select("SELECT COUNT(*) FROM ll_inventory_ledger WHERE customer_id=#{customerId} AND pallet_id=#{palletId} " +
|
||||
"AND business_type IN ('RELABEL_PALLET_DISASSEMBLE','PALLET_ASSEMBLE','PALLET_DISASSEMBLE','PALLET_SWAP','PALLET_UNBIND')")
|
||||
int movements(@Param("customerId") String customerId, @Param("palletId") Long palletId);
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.mapper.*;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.*;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.math.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** Measurements are audited relabel facts, valid only for the measured inventory composition/version. */
|
||||
@Service
|
||||
public class RelabelPalletMeasurementService {
|
||||
@Resource private RelabelPalletMeasurementMapper mapper;
|
||||
@Resource private RelabelApplicationMapper applications;
|
||||
@Resource private RelabelItemMapper items;
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private PdaRelabelTaskMapper tasks;
|
||||
@Resource private ObjectMapper json;
|
||||
public record Measure(@NotBlank @Size(max=64) String relabelNo, @NotBlank @Size(max=128) String palletCode,
|
||||
@NotBlank @Size(max=128) String idempotencyKey, @NotBlank @Size(max=32768) String stockVersion,
|
||||
@NotNull @DecimalMin("0.01") @Digits(integer=6,fraction=2) BigDecimal lengthCm,
|
||||
@NotNull @DecimalMin("0.01") @Digits(integer=6,fraction=2) BigDecimal widthCm,
|
||||
@NotNull @DecimalMin("0.01") @Digits(integer=6,fraction=2) BigDecimal heightCm,
|
||||
@NotNull @DecimalMin("0.01") @Digits(integer=9,fraction=3) BigDecimal grossWeightKg) {}
|
||||
public record Measurement(Long palletId, String palletCode, long packages, String stockVersion,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, BigDecimal grossWeightKg,
|
||||
BigDecimal volumeCbm, boolean inherited) {}
|
||||
|
||||
public List<Measurement> list(PdaWarehouseIdentity identity, String relabelNo) {
|
||||
return snapshots(scoped(identity, relabelNo, false));
|
||||
}
|
||||
private RelabelApplication scoped(PdaWarehouseIdentity identity, String no, boolean lock) {
|
||||
var app = lock ? applications.lockAnyByRelabelNo(no) : applications.findAnyByRelabelNo(no);
|
||||
if (app == null || !Objects.equals(app.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId())))
|
||||
throw conflict("换标单不属于当前海外仓");
|
||||
return app;
|
||||
}
|
||||
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public List<Measurement> save(PdaWarehouseIdentity identity, Measure request) {
|
||||
var app = scoped(identity, request.relabelNo(), true);
|
||||
if (!Set.of("PROCESSING", "READY_FOR_OUTBOUND").contains(app.getStatus())
|
||||
|| app.getOutboundOrderNo() != null && !app.getOutboundOrderNo().isBlank()
|
||||
|| app.getFulfillmentStatus() != null && !Set.of("WAIT_RELABEL", "WAIT_PRE_DISPATCH").contains(app.getFulfillmentStatus()))
|
||||
throw conflict("已排车或出库的换标单不能修改测量");
|
||||
var task = tasks.find(app.getCustomerId(), app.getId());
|
||||
if (task == null || "PROCESSING".equals(app.getStatus()) && (!"PROCESSING".equals(task.getStatus()) || !identity.operatorKey().equals(task.getOperatorKey())))
|
||||
throw conflict("请先认领当前换标任务");
|
||||
var records = histories(app);
|
||||
String payload = write(request);
|
||||
for (var history : records) if ("PALLET_MEASURE".equals(history.getAction())) {
|
||||
var data = tree(history);
|
||||
if (request.idempotencyKey().equals(data.path("request").path("idempotencyKey").asText())) {
|
||||
if (!payload.equals(write(json.convertValue(data.path("request"), Measure.class))))
|
||||
throw conflict("测量幂等键不能用于不同数据");
|
||||
return snapshots(app);
|
||||
}
|
||||
}
|
||||
mapper.lockStock(app.getCustomerId(), app.getOverseasWarehouseId(), request.palletCode());
|
||||
var current = snapshots(app).stream().filter(p -> p.palletCode().equals(request.palletCode())).findFirst()
|
||||
.orElseThrow(() -> conflict("托盘不属于本换标单或已经清空"));
|
||||
if (!current.stockVersion().equals(request.stockVersion())) throw conflict("托盘库存已变化,请刷新后重新测量");
|
||||
var measured = new Measurement(current.palletId(), current.palletCode(), current.packages(), current.stockVersion(),
|
||||
request.lengthCm(), request.widthCm(), request.heightCm(), request.grossWeightKg(),
|
||||
request.lengthCm().multiply(request.widthCm()).multiply(request.heightCm()).movePointLeft(6).setScale(6, RoundingMode.HALF_UP), false);
|
||||
var history = new PdaRelabelHistory(); history.setId(IdWorker.getId()); history.setTaskId(task.getId());
|
||||
history.setApplicationId(app.getId()); history.setCustomerId(app.getCustomerId()); history.setAction("PALLET_MEASURE");
|
||||
history.setOperatorKey(identity.operatorKey()); history.setOperatorName(identity.operatorName());
|
||||
history.setFromStatus(task.getStatus()); history.setToStatus(task.getStatus()); history.setCreatedAt(LocalDateTime.now());
|
||||
history.setDetailJson(write(Map.of("request", request, "measurement", measured))); histories.insert(history);
|
||||
return snapshots(app);
|
||||
}
|
||||
public List<Measurement> snapshots(RelabelApplication app) {
|
||||
Long transferId = mapper.transfer(app.getCustomerId(), app.getRelabelNo());
|
||||
if (transferId == null) return List.of(); // Other relabel sources retain their own physical measurement contract.
|
||||
var stock = mapper.stock(app.getCustomerId(), app.getOverseasWarehouseId());
|
||||
var originals = mapper.originals(app.getCustomerId(), transferId);
|
||||
var involved = new HashSet<Long>(); originals.forEach(p -> involved.add(p.palletId()));
|
||||
var positions = items.selectList(new QueryWrapper<com.baiyue.lastleg.relabel.entity.RelabelItem>()
|
||||
.eq("customer_id", app.getCustomerId()).eq("application_id", app.getId()));
|
||||
var positionIds = positions.stream().map(i -> i.getSourceInventoryPositionId()).toList();
|
||||
stock.stream().filter(p -> positionIds.contains(p.id())).forEach(p -> involved.add(p.palletId()));
|
||||
var records = histories(app);
|
||||
var codes = new HashSet<String>();
|
||||
for (var h : records) if ("PALLET_BIND".equals(h.getAction())) {
|
||||
tree(h).path("sourcePallets").forEach(p -> codes.add(p.asText()));
|
||||
tree(h).path("targetPallets").forEach(p -> codes.add(p.asText()));
|
||||
}
|
||||
stock.stream().filter(p -> codes.contains(p.palletNo())).forEach(p -> involved.add(p.palletId()));
|
||||
var result = new ArrayList<Measurement>();
|
||||
for (Long pallet : involved.stream().filter(Objects::nonNull).sorted().toList()) {
|
||||
var rows = stock.stream().filter(p -> pallet.equals(p.palletId()) && p.packages() > 0).toList();
|
||||
long count = rows.stream().mapToLong(p -> p.packages()).sum(); if (count == 0) continue;
|
||||
String version = rows.stream().map(p -> p.id()+":"+p.version()+":"+p.packages()).collect(java.util.stream.Collectors.joining(","));
|
||||
Measurement value = null;
|
||||
for (var h : records) if ("PALLET_MEASURE".equals(h.getAction())) {
|
||||
var measured = json.convertValue(tree(h).path("measurement"), Measurement.class);
|
||||
if (pallet.equals(measured.palletId()) && version.equals(measured.stockVersion())) value = measured;
|
||||
}
|
||||
if (value == null && mapper.movements(app.getCustomerId(), pallet) == 0) {
|
||||
var original = originals.stream().filter(p -> pallet.equals(p.palletId()) && p.packages() == count).findFirst().orElse(null);
|
||||
if (original != null) value = new Measurement(pallet, rows.get(0).palletNo(), count, version,
|
||||
original.lengthCm(), original.widthCm(), original.heightCm(), original.grossWeightKg(), original.volumeCbm(), true);
|
||||
}
|
||||
result.add(value == null ? new Measurement(pallet, rows.get(0).palletNo(), count, version, null,null,null,null,null,false) : value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public void requireMeasured(RelabelApplication app) {
|
||||
for (var p : snapshots(app)) if (p.grossWeightKg() == null || p.volumeCbm() == null
|
||||
|| p.lengthCm() == null || p.widthCm() == null || p.heightCm() == null
|
||||
|| p.grossWeightKg().signum() <= 0 || p.volumeCbm().signum() <= 0)
|
||||
throw conflict("托盘 " + p.palletCode() + " 拆合托后尚未确认实测尺寸和重量,请在 PDA 补测(含原托盘剩余货物)");
|
||||
}
|
||||
private List<PdaRelabelHistory> histories(RelabelApplication app) {
|
||||
return histories.selectList(new QueryWrapper<PdaRelabelHistory>().eq("customer_id", app.getCustomerId())
|
||||
.eq("application_id", app.getId()).orderByAsc("created_at","id"));
|
||||
}
|
||||
private com.fasterxml.jackson.databind.JsonNode tree(PdaRelabelHistory h) {
|
||||
try { return json.readTree(h.getDetailJson()); } catch (Exception e) { throw conflict("托盘测量记录无法读取"); }
|
||||
}
|
||||
private String write(Object value) { try { return json.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException(e); } }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT,message); }
|
||||
}
|
||||
@@ -220,9 +220,44 @@ paths:
|
||||
/api/pda/v1/last-leg/pallet-operations/relabel-disassemble:
|
||||
post:
|
||||
summary: 按换标箱唛拆托并绑定目标托盘
|
||||
description: PDA选择原入仓单库存行并扫描实际来源托盘,输入拆出箱数,核对本批cartonMarks后扫描目标托盘。传actualSource时以其数量和版本为准,同入仓余额及批次内原子调整本单冻结记账位置,再移动冻结库存和换标明细;不新增入库、不改变库存归属、不占用其他单据冻结量。每次一个来源库存行,可分批处理多个托盘。已有出库单时不允许此数量重分配。未传actualSource的箱唛模式仍要求已有来源仓位一致。
|
||||
description: PDA选择原入仓单库存行并扫描实际来源托盘,输入拆出箱数,核对本批cartonMarks后扫描目标托盘。传actualSource时以其数量和版本为准,同入仓余额及批次内原子调整本单冻结记账位置,再移动冻结库存和换标明细;不新增入库、不改变库存归属、不占用其他单据冻结量。每次一个来源库存行,可分批处理多个托盘。已有出库单时不允许此数量重分配。未传actualSource的箱唛模式仍要求已有来源仓位一致。普通库存整托全部换标允许目标与来源为同一托盘,校验整托数量和本单冻结归属后仅记录绑定、不搬移库存;部分换标必须使用新托盘。货物和包装不变时可继承原托盘测量,拆合托后新托盘和原托余货均须重新实测。
|
||||
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/RelabelPalletDisassemble' } } } }
|
||||
responses: { '200': { description: 箱唛级拆托绑定结果 }, '409': { description: 箱唛、库存或托盘版本冲突 } }
|
||||
/api/pda/v1/last-leg/pallet-operations/relabel-pallet-measurements:
|
||||
post:
|
||||
summary: 查询仓储转运换标涉及托盘的最终测量及库存版本
|
||||
description: 权限 lastleg:pda:operate 及海外PDA角色;仅Token绑定仓。整托原托盘且没有拆合托流水、箱数未变时沿用原始测量;新托盘和拆合托后的原托盘余货需要补测。测量保存为PALLET_MEASURE作业历史,库存版本变化后失效。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [relabelNo]
|
||||
properties:
|
||||
relabelNo: { type: string, maxLength: 64 }
|
||||
responses: { '200': { description: 托盘业务码、箱数、stockVersion、长宽高cm、毛重kg、体积m³及inherited;其他来源换标返回空列表 }, '409': { description: 不属于当前仓 } }
|
||||
/api/pda/v1/last-leg/pallet-operations/relabel-pallet-measure:
|
||||
post:
|
||||
summary: 确认仓储转运换标托盘最终实测
|
||||
description: 已认领的操作员可在处理中保存;待出库但未排车亦可补测。保存当前stockVersion及实测长宽高、毛重,服务端计算体积;相同幂等键异载荷拒绝,库存变化要求刷新。原托盘包装变化同样使用此接口重新测量。排车前核验所有相关非空托盘,包括原托盘余货。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [relabelNo, palletCode, stockVersion, idempotencyKey, lengthCm, widthCm, heightCm, grossWeightKg]
|
||||
properties:
|
||||
relabelNo: { type: string, maxLength: 64 }
|
||||
palletCode: { type: string, maxLength: 128 }
|
||||
stockVersion: { type: string, description: 查询返回的当前库存版本指纹 }
|
||||
idempotencyKey: { type: string, maxLength: 128 }
|
||||
lengthCm: { type: number, minimum: 0.01, multipleOf: 0.01 }
|
||||
widthCm: { type: number, minimum: 0.01, multipleOf: 0.01 }
|
||||
heightCm: { type: number, minimum: 0.01, multipleOf: 0.01 }
|
||||
grossWeightKg: { type: number, minimum: 0.01, multipleOf: 0.001 }
|
||||
responses: { '200': { description: 已保存并返回全部相关托盘测量 }, '409': { description: 版本、归属、状态、认领人或幂等载荷冲突 } }
|
||||
/api/pda/v1/last-leg/pallet-operations/swap:
|
||||
post:
|
||||
summary: 将整托库存更换到空目标托盘
|
||||
|
||||
+13
@@ -5,6 +5,8 @@ import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -187,6 +189,17 @@ class DispatchCandidateMapperContractTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void relabelCandidatesReadWeightFromWarehouseTransferPalletSnapshot() throws Exception {
|
||||
String source = Files.readString(Path.of("src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java"));
|
||||
int start = source.indexOf("List<DispatchPalletCandidate> relabelAvailable");
|
||||
int end = source.indexOf("List<DispatchPalletCandidate> nativeReturnRelabelAvailable");
|
||||
String sql = source.substring(source.lastIndexOf("@Select", start), end);
|
||||
assertTrue(sql.contains("MAX(wtp.gross_weight_kg) AS grossWeightKg"));
|
||||
assertTrue(sql.contains("MAX(wtp.volume_cbm) AS volumeCbm"));
|
||||
assertTrue(sql.contains("wtp.warehouse_pallet_id=pos.pallet_id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateGrossWeightUsesPhysicalSourcesAndNeverBillingVolumeWeight() throws Exception {
|
||||
for (Method method : List.of(
|
||||
|
||||
+5
@@ -25,6 +25,11 @@ class DispatchPhysicalWeightTest {
|
||||
int to = sql.indexOf(" AS grossWeightKg,");
|
||||
if (from < 0 || to < 0) continue;
|
||||
String projection = sql.substring(from + "AS volumeWeightKg,".length(), to);
|
||||
if (projection.contains("wtp.gross_weight_kg")) {
|
||||
assertThat(jdbc.queryForObject("SELECT " + projection + " FROM (SELECT CAST(NULL AS DECIMAL(12,3)) gross_weight_kg) wtp", BigDecimal.class)).isNull();
|
||||
assertThat(jdbc.queryForObject("SELECT " + projection + " FROM (SELECT 80 gross_weight_kg) wtp", BigDecimal.class)).isEqualByComparingTo("80");
|
||||
continue;
|
||||
}
|
||||
String query = "SELECT " + projection + " FROM (SELECT '10' AS customer_id) ip "
|
||||
+ "CROSS JOIN (SELECT 1 AS order_id,10 AS volume_cbm) l "
|
||||
+ "CROSS JOIN (SELECT 1 AS line_id,5 AS allocated_volume_cbm) a "
|
||||
|
||||
+2
@@ -65,11 +65,13 @@ class DispatchServiceTest {
|
||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
|
||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
|
||||
@Mock ReturnRelabelDispatchStock returnRelabelStock;
|
||||
@Mock com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService relabelMeasurements;
|
||||
private DispatchService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DispatchService();
|
||||
ReflectionTestUtils.setField(service, "relabelMeasurements", relabelMeasurements);
|
||||
ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow);
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
ReflectionTestUtils.setField(service, "returnRelabelStock", returnRelabelStock);
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class PdaPalletOperationControllerContractTest {
|
||||
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
|
||||
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
|
||||
assertEquals(Set.of("/pallets/resolve", "/relabel-pallet-label", "/assemble", "/disassemble",
|
||||
"/relabel-disassemble", "/relabel-sources", "/swap", "/unbind"), paths);
|
||||
"/relabel-disassemble", "/relabel-sources", "/relabel-pallet-measure", "/relabel-pallet-measurements", "/swap", "/unbind"), paths);
|
||||
}
|
||||
|
||||
@Test void palletContentAnnotationUsesNativeGreaterThanOperator() throws Exception {
|
||||
|
||||
+32
-3
@@ -138,14 +138,14 @@ class PdaPalletOperationServiceTest {
|
||||
new PalletOperationRequests.PalletExpectation("SOURCE", 1),
|
||||
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("BOX-1"), "rebind");
|
||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
|
||||
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
|
||||
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request, false));
|
||||
verifyNoInteractions(physical);
|
||||
var binding = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
|
||||
binding.setDetailJson("{\"round\":\"INITIAL\",\"cartonMarks\":[\"BOX-1\"]}");
|
||||
when(workHistory.selectList(any())).thenReturn(List.of(binding));
|
||||
task.setReviewedAt(LocalDateTime.of(2026, 10, 12, 12, 0));
|
||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service,
|
||||
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request));
|
||||
"bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request, false));
|
||||
task.setReviewedAt(null);
|
||||
when(physical.resolveLocation(88L, 1020L)).thenReturn(
|
||||
new PalletPhysicalGateway.LocationReference(1020L, "SOURCE-LOC", "STORAGE", "ACTIVE"));
|
||||
@@ -155,7 +155,7 @@ class PdaPalletOperationServiceTest {
|
||||
when(physical.resolve("TARGET")).thenReturn(target);
|
||||
var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation)
|
||||
ReflectionTestUtils.invokeMethod(service, "bindReturnRelabelPallet", identity(), app,
|
||||
List.of(item), source, target, request);
|
||||
List.of(item), source, target, request, false);
|
||||
assertEquals("TARGET", box.getPalletCode());
|
||||
assertEquals(0, result.pallets().get(0).totalPackages());
|
||||
assertEquals(2, result.pallets().get(1).totalPackages());
|
||||
@@ -584,6 +584,35 @@ class PdaPalletOperationServiceTest {
|
||||
assertThrows(ResponseStatusException.class, () -> service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "different")));
|
||||
}
|
||||
|
||||
@Test void wholeRelabelCanRetainOriginalPalletWithoutMovingStockAndPartialCannot() {
|
||||
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
|
||||
ReflectionTestUtils.setField(service,"relabelWorkTasks",tasks); ReflectionTestUtils.setField(service,"relabelWorkHistory",histories);
|
||||
var app = new RelabelApplication();app.setId(600L);app.setCustomerId("100");app.setRelabelNo("RL-1");
|
||||
app.setOverseasWarehouseId("88");app.setStatus("READY_FOR_OUTBOUND");app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
|
||||
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
|
||||
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(app);
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();task.setId(900L);task.setStatus("COMPLETED");
|
||||
when(tasks.find("100",600L)).thenReturn(task);
|
||||
var item = relabelItem(701L,"CARTON-A",100L);
|
||||
when(relabelItems.selectList(any())).thenReturn(List.of(item));
|
||||
when(relabelApplications.selectById(600L)).thenReturn(app);
|
||||
when(physical.resolve("SOURCE")).thenReturn(pallet(10L,"SOURCE",88L,0));
|
||||
when(physical.resolveLocation(88L,1010L)).thenReturn(new PalletPhysicalGateway.LocationReference(1010L,"LOC-1010","STORAGE","ACTIVE"));
|
||||
var position = position(100L,10L,"SOURCE",1,0);when(positions.selectById(100L)).thenReturn(position);
|
||||
var row = row(100L,10L,0,0);row.setHoldPackages(1L);
|
||||
when(mapper.lockPosition("88",100L)).thenReturn(row);when(mapper.content("88",10L)).thenReturn(List.of(row));
|
||||
when(mapper.palletPackages("88",10L)).thenReturn(1L);
|
||||
var request = new PalletOperationRequests.RelabelDisassemble("retain-1","RL-1",
|
||||
new PalletOperationRequests.PalletExpectation("SOURCE",0),new PalletOperationRequests.PalletExpectation("SOURCE",0),List.of("CARTON-A"),"整托贴标");
|
||||
var result = service.relabelDisassemble(identity(),request);
|
||||
assertEquals("COMPLETED",result.status()); assertEquals(1,result.pallets().size());
|
||||
verify(positions,never()).updateById(any(InventoryPosition.class)); verifyNoInteractions(relabelPalletMutations,ledgers);
|
||||
verify(histories).insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class));
|
||||
when(mapper.palletPackages("88",10L)).thenReturn(2L);
|
||||
assertTrue(assertThrows(ResponseStatusException.class,() -> service.relabelDisassemble(identity(),request)).getReason().contains("部分贴标"));
|
||||
}
|
||||
|
||||
private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); }
|
||||
private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) {
|
||||
return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.relabel.mapper.*;
|
||||
import com.baiyue.lastleg.relabel.entity.*;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.*;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.*;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class RelabelPalletMeasurementServiceTest {
|
||||
final RelabelPalletMeasurementService service = new RelabelPalletMeasurementService();
|
||||
final RelabelPalletMeasurementMapper mapper = mock(RelabelPalletMeasurementMapper.class);
|
||||
final RelabelApplicationMapper apps = mock(RelabelApplicationMapper.class);
|
||||
final RelabelItemMapper items = mock(RelabelItemMapper.class);
|
||||
final PdaRelabelHistoryMapper histories = mock(PdaRelabelHistoryMapper.class);
|
||||
final PdaRelabelTaskMapper tasks = mock(PdaRelabelTaskMapper.class);
|
||||
final RelabelApplication app = new RelabelApplication();
|
||||
final List<PdaRelabelHistory> records = new ArrayList<>();
|
||||
final ObjectMapper json = new ObjectMapper();
|
||||
@BeforeEach void setup() {
|
||||
ReflectionTestUtils.setField(service,"mapper",mapper); ReflectionTestUtils.setField(service,"applications",apps);
|
||||
ReflectionTestUtils.setField(service,"items",items); ReflectionTestUtils.setField(service,"histories",histories);
|
||||
ReflectionTestUtils.setField(service,"tasks",tasks); ReflectionTestUtils.setField(service,"json",json);
|
||||
app.setId(1L);app.setCustomerId("7");app.setRelabelNo("RL-1");app.setOverseasWarehouseId("92");
|
||||
app.setStatus("READY_FOR_OUTBOUND");app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
|
||||
when(apps.findAnyByRelabelNo("RL-1")).thenReturn(app);when(apps.lockAnyByRelabelNo("RL-1")).thenReturn(app);
|
||||
when(mapper.transfer("7","RL-1")).thenReturn(10L);
|
||||
when(mapper.originals("7",10L)).thenReturn(List.of(new RelabelPalletMeasurementMapper.Original(100L,"OLD",10,
|
||||
new BigDecimal("100"),new BigDecimal("100"),new BigDecimal("100"),new BigDecimal("80"),BigDecimal.ONE)));
|
||||
when(mapper.stock("7","92")).thenReturn(List.of(stock(1,100,"OLD",10,1)));
|
||||
when(items.selectList(any())).thenReturn(List.of());
|
||||
when(histories.selectList(any())).thenAnswer(i -> List.copyOf(records));
|
||||
when(histories.insert(any(PdaRelabelHistory.class))).thenAnswer(i -> { records.add(i.getArgument(0));return 1; });
|
||||
var task = new PdaRelabelTask(); task.setId(2L); task.setStatus("COMPLETED");
|
||||
when(tasks.find("7",1L)).thenReturn(task);
|
||||
}
|
||||
RelabelPalletMeasurementMapper.Stock stock(long id,long pallet,String code,long count,long version) {
|
||||
return new RelabelPalletMeasurementMapper.Stock(id,pallet,code,count,version);
|
||||
}
|
||||
@Test void wholeOriginalPalletInheritsWithoutInventingOrRescalingMeasurements() {
|
||||
var p = service.snapshots(app).get(0);assertTrue(p.inherited()); assertEquals(new BigDecimal("80"),p.grossWeightKg());
|
||||
assertEquals(BigDecimal.ONE,p.volumeCbm());assertDoesNotThrow(() -> service.requireMeasured(app));
|
||||
}
|
||||
@Test void partialSplitRequiresBothNewPalletAndOriginalRemainderMeasurements() throws Exception {
|
||||
split(); var values = service.snapshots(app);assertEquals(2,values.size());
|
||||
assertTrue(values.stream().allMatch(p -> p.volumeCbm()==null && p.grossWeightKg()==null));
|
||||
assertThrows(ResponseStatusException.class,() -> service.requireMeasured(app));
|
||||
}
|
||||
@Test void explicitFinalMeasurementsAreAuditedAndReplayDoesNotDuplicateFacts() throws Exception {
|
||||
split();
|
||||
for (var p : service.snapshots(app)) {
|
||||
var request = new RelabelPalletMeasurementService.Measure("RL-1",p.palletCode(),"measure-"+p.palletId(),p.stockVersion(),
|
||||
new BigDecimal("100"),new BigDecimal("50"),new BigDecimal("50"),new BigDecimal("40"));
|
||||
service.save(identity(),request);service.save(identity(),request);
|
||||
}
|
||||
assertEquals(3,records.size()); // One binding, two measurements.
|
||||
assertDoesNotThrow(() -> service.requireMeasured(app));
|
||||
assertTrue(service.snapshots(app).stream().allMatch(p -> p.volumeCbm().compareTo(new BigDecimal("0.25"))==0));
|
||||
when(mapper.stock("7","92")).thenReturn(List.of(stock(1,100,"OLD",5,3),stock(2,200,"NEW",5,1)));
|
||||
assertThrows(ResponseStatusException.class,() -> service.requireMeasured(app));
|
||||
}
|
||||
@Test void staleAndForeignMeasurementsAndAllocatedOrdersAreRejected() {
|
||||
var request = new RelabelPalletMeasurementService.Measure("RL-1","OLD","key","stale",
|
||||
BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE);
|
||||
assertThrows(ResponseStatusException.class,() -> service.save(identity(),request));
|
||||
app.setOverseasWarehouseId("93"); assertThrows(ResponseStatusException.class,() -> service.list(identity(),"RL-1"));
|
||||
app.setOverseasWarehouseId("92");app.setFulfillmentStatus("PRE_DISPATCH_CREATED");
|
||||
assertThrows(ResponseStatusException.class,() -> service.save(identity(),request));
|
||||
verify(histories,never()).insert(any(PdaRelabelHistory.class));
|
||||
}
|
||||
private void split() throws Exception {
|
||||
when(mapper.stock("7","92")).thenReturn(List.of(stock(1,100,"OLD",5,2),stock(2,200,"NEW",5,1)));
|
||||
when(mapper.movements("7",100L)).thenReturn(1);
|
||||
var binding = new PdaRelabelHistory();binding.setAction("PALLET_BIND");
|
||||
binding.setDetailJson(json.writeValueAsString(Map.of("sourcePallets",List.of("OLD"),"targetPallets",List.of("NEW")))); records.add(binding);
|
||||
}
|
||||
private PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("operator","现场人员",92L); }
|
||||
}
|
||||
Reference in New Issue
Block a user