贴标申请,仓储转运
This commit is contained in:
@@ -4,4 +4,9 @@ import java.time.LocalDateTime;
|
||||
|
||||
/** A relabel application keeps its own status within the received parent order. */
|
||||
public record RelabelSubOrder(Long lastlegOrderId, String relabelNo, String status,
|
||||
Integer quantity, LocalDateTime createdAt) { }
|
||||
Integer quantity, String quantityUnit, LocalDateTime createdAt) {
|
||||
public RelabelSubOrder(Long lastlegOrderId, String relabelNo, String status,
|
||||
Integer quantity, LocalDateTime createdAt) {
|
||||
this(lastlegOrderId, relabelNo, status, quantity, "件", createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
"WHERE g.container_booking_id=ll_order.source_container_booking_id " +
|
||||
"AND g.node_sequence>=40 AND g.actual_at IS NOT NULL) THEN 1 ELSE 0 END END)={0})";
|
||||
|
||||
@Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity,r.created_at " +
|
||||
@Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity, " +
|
||||
"CASE WHEN NOT EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=r.id AND ri.operation_type<>'CARTON_LABEL') THEN '箱' ELSE '件' END AS quantity_unit,r.created_at " +
|
||||
"FROM ll_relabel_application r JOIN ll_order o ON o.id=r.lastleg_order_id AND o.customer_id=r.customer_id " +
|
||||
"WHERE r.lastleg_order_id IN <foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"ORDER BY r.created_at,r.id</script>")
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ public final class PalletOperationRequests {
|
||||
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
/** actualSource为现场确认的原入仓库存行;cartonMarks确认本批目标绑定,不推断历史箱唛位置。 */
|
||||
/** source为空时表示退货箱没有来源托盘,现场仅扫描箱唛后绑定目标托盘;actualSource为现场确认的原入仓库存行。 */
|
||||
public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@NotBlank @Size(max = 64) String relabelNo,
|
||||
@Valid PalletExpectation source,
|
||||
|
||||
+33
-16
@@ -105,13 +105,16 @@ public class PdaPalletOperationService {
|
||||
|| !Objects.equals(carton.getOverseasWarehouseId(), app.getOverseasWarehouseId())
|
||||
|| !Objects.equals(carton.getStatus(), "RELABEL_PENDING"))
|
||||
throw conflict("退货换标来源托盘不存在或已被占用");
|
||||
PalletPhysicalGateway.PalletReference pallet = physical.resolve(carton.getPalletCode());
|
||||
if (pallet == null || pallet.warehouseId() != identity.overseasWarehouseId())
|
||||
String palletCode = text(carton.getPalletCode());
|
||||
PalletPhysicalGateway.PalletReference pallet = palletCode.isBlank() || "UNBOUND".equalsIgnoreCase(palletCode)
|
||||
? null : physical.resolve(palletCode);
|
||||
if (palletCode.isBlank() || "UNBOUND".equalsIgnoreCase(palletCode)) palletCode = "UNBOUND";
|
||||
if (pallet != null && pallet.warehouseId() != identity.overseasWarehouseId())
|
||||
throw conflict("退货换标来源托盘不属于当前仓库");
|
||||
long quantity = entry.getValue().stream().anyMatch(item -> "RETURN_BULK_SKU_LABEL".equals(item.getOperationType()))
|
||||
? value(carton.getTotalQuantity()) : 1;
|
||||
result.add(new PalletOperationResponses.RelabelSource(carton.getId(), 0L,
|
||||
pallet.palletId(), pallet.palletCode(), null, carton.getSourceBatchNo(), quantity, quantity,
|
||||
pallet == null ? null : pallet.palletId(), palletCode, null, carton.getSourceBatchNo(), quantity, quantity,
|
||||
entry.getValue().stream().map(RelabelItem::getCartonMark).filter(Objects::nonNull).toList()));
|
||||
}
|
||||
return result;
|
||||
@@ -225,11 +228,15 @@ public class PdaPalletOperationService {
|
||||
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("换标单不属于 Token 冻结的海外仓");
|
||||
}
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
request.source(), List.of(request.target()));
|
||||
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode()));
|
||||
PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode()));
|
||||
if (source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同");
|
||||
if (request.target() == null) throw badRequest("目标托盘不能为空");
|
||||
PalletPhysicalGateway.PalletReference target = resolvePallets(identity.overseasWarehouseId(),
|
||||
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);
|
||||
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(value -> !value.isEmpty()).toList();
|
||||
if (marks.size() != request.cartonMarks().size() || new HashSet<>(marks).size() != marks.size()) {
|
||||
throw badRequest("箱唛不能为空且不能重复");
|
||||
@@ -245,6 +252,13 @@ public class PdaPalletOperationService {
|
||||
Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>();
|
||||
Map<Long, InventoryPosition> anchors = new LinkedHashMap<>();
|
||||
boolean returnRelabel = selected.stream().allMatch(item -> item.getReturnCartonId() != null);
|
||||
boolean unboundReturnRelabel = returnRelabel && selected.stream().allMatch(item -> {
|
||||
ReturnInventoryCarton carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
return carton != null && (!org.springframework.util.StringUtils.hasText(carton.getPalletCode())
|
||||
|| "UNBOUND".equalsIgnoreCase(carton.getPalletCode()));
|
||||
});
|
||||
if (unboundReturnRelabel && source != null) throw conflict("无来源托盘的退货箱不能提交来源托盘");
|
||||
if (!unboundReturnRelabel && source == null) throw conflict("请先确认来源托盘");
|
||||
InventoryPosition actual = returnRelabel || request.actualSource() == null
|
||||
? null : positions.selectById(request.actualSource().positionId());
|
||||
if (!returnRelabel && request.actualSource() != null && (actual == null
|
||||
@@ -269,7 +283,7 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
}
|
||||
if (returnRelabel) {
|
||||
return bindReturnRelabelPallet(identity, application, selected, source, target, request);
|
||||
return bindReturnRelabelPallet(identity, application, selected, source, target, request, unboundReturnRelabel);
|
||||
}
|
||||
for (RelabelItem item : selected) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
|
||||
@@ -313,7 +327,7 @@ public class PdaPalletOperationService {
|
||||
private PalletOperationResponses.Operation bindReturnRelabelPallet(PdaWarehouseIdentity identity,
|
||||
RelabelApplication application, List<RelabelItem> selected,
|
||||
PalletPhysicalGateway.PalletReference source, PalletPhysicalGateway.PalletReference target,
|
||||
PalletOperationRequests.RelabelDisassemble request) {
|
||||
PalletOperationRequests.RelabelDisassemble request, boolean unboundSource) {
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
|
||||
Set<String> alreadyBound = boundRelabelMarks(application);
|
||||
boolean rebind = request.actualSource() == null && selected.stream().allMatch(item -> alreadyBound.contains(item.getCartonMark()));
|
||||
@@ -324,14 +338,15 @@ public class PdaPalletOperationService {
|
||||
for (RelabelItem item : selected) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(carton.getPalletCode(), source.palletCode())
|
||||
|| (unboundSource ? (org.springframework.util.StringUtils.hasText(carton.getPalletCode()) && !"UNBOUND".equalsIgnoreCase(carton.getPalletCode()))
|
||||
: !Objects.equals(carton.getPalletCode(), source.palletCode()))
|
||||
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|
||||
|| !Objects.equals(carton.getStatus(), "RELABEL_PENDING"))
|
||||
throw conflict("退货换标来源托盘或库存状态已变化,请刷新后重试");
|
||||
quantity += value(item.getNotifiedPackageCount());
|
||||
cartons.add(carton);
|
||||
}
|
||||
if (!rebind && (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|
||||
if (!rebind && !unboundSource && (request.actualSource() == null || request.actualSource().positionId() != cartons.get(0).getId()
|
||||
|| request.actualSource().packages() != quantity))
|
||||
throw conflict("退货换标拆出数量必须等于本批已完成贴标数量");
|
||||
String key = required(request.idempotencyKey(), "幂等键不能为空");
|
||||
@@ -345,7 +360,8 @@ public class PdaPalletOperationService {
|
||||
RelabelItem item = selected.get(index);
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(carton.getPalletCode(), source.palletCode())
|
||||
|| (unboundSource ? (org.springframework.util.StringUtils.hasText(carton.getPalletCode()) && !"UNBOUND".equalsIgnoreCase(carton.getPalletCode()))
|
||||
: !Objects.equals(carton.getPalletCode(), source.palletCode()))
|
||||
|| !Objects.equals(carton.getRelabelApplicationId(), application.getId())
|
||||
|| !"RELABEL_PENDING".equals(carton.getStatus()))
|
||||
throw conflict("退货换标来源库存已变化,请刷新后重试");
|
||||
@@ -363,9 +379,10 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
}
|
||||
recordReturnRelabelBinding(identity, application, selected, target.palletCode());
|
||||
List<PalletOperationResponses.PalletContent> contents = List.of(
|
||||
content(identity.overseasWarehouseId(), physical.resolve(source.palletCode())),
|
||||
content(identity.overseasWarehouseId(), physical.resolve(target.palletCode())));
|
||||
List<PalletOperationResponses.PalletContent> contents = source == null
|
||||
? List.of(content(identity.overseasWarehouseId(), physical.resolve(target.palletCode())))
|
||||
: List.of(content(identity.overseasWarehouseId(), physical.resolve(source.palletCode())),
|
||||
content(identity.overseasWarehouseId(), physical.resolve(target.palletCode())));
|
||||
PalletOperationResponses.Operation result = new PalletOperationResponses.Operation(operation.getId(),
|
||||
operation.getOperationType(), key, "COMPLETED", false, LocalDateTime.now(), contents);
|
||||
operation.setStatus("COMPLETED"); operation.setResultJson(write(result));
|
||||
|
||||
@@ -19,7 +19,8 @@ public final class RelabelResponses {
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
|
||||
String reviewChannel, String reviewRemark, LocalDateTime createdAt,
|
||||
LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt, LocalDateTime outboundAt) {}
|
||||
LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt, LocalDateTime outboundAt,
|
||||
String productNameZh, String labelRequirement) {}
|
||||
public record Milestones(LocalDateTime reviewedAt, LocalDateTime relabelCompletedAt,
|
||||
LocalDateTime outboundAt) {}
|
||||
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
|
||||
|
||||
+10
-2
@@ -1187,7 +1187,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
private RelabelResponses.Summary summary(RelabelApplication value, List<RelabelItem> detailItems) {
|
||||
LastlegOrder order = orders.selectById(value.getLastlegOrderId());
|
||||
RelabelResponses.Milestones milestones = applications.findMilestones(value.getCustomerId(), value.getId());
|
||||
String skuCodes = detailItems.stream().map(RelabelItem::getSkuCode).filter(Objects::nonNull).distinct().collect(Collectors.joining(","));
|
||||
String skuCodes = detailItems.stream().flatMap(item -> {
|
||||
List<RelabelResponses.SkuItem> skuItems = readSkuItems(item.getSkuRelabelItemsJson());
|
||||
return skuItems.isEmpty() ? java.util.Arrays.stream(Objects.requireNonNullElse(item.getSkuCode(), "").split(","))
|
||||
: skuItems.stream().map(RelabelResponses.SkuItem::skuCode);
|
||||
}).filter(StringUtils::hasText).map(String::trim).distinct().collect(Collectors.joining(","));
|
||||
return new RelabelResponses.Summary(value.getId(), value.getRelabelNo(), value.getLastlegOrderId(),
|
||||
order == null ? null : order.getOrderNo(), value.getCustomerOrderNo(),
|
||||
value.getOverseasWarehouseId(),
|
||||
@@ -1206,7 +1210,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
null,
|
||||
value.getCreatedAt(), milestones == null ? null : milestones.reviewedAt(),
|
||||
milestones == null ? null : milestones.relabelCompletedAt(),
|
||||
milestones == null ? null : milestones.outboundAt());
|
||||
milestones == null ? null : milestones.outboundAt(),
|
||||
detailItems.stream().map(RelabelItem::getProductNameZh).filter(StringUtils::hasText)
|
||||
.map(String::trim).distinct().collect(Collectors.joining("、")),
|
||||
detailItems.stream().map(RelabelItem::getLabelRequirement).filter(StringUtils::hasText)
|
||||
.map(String::trim).distinct().collect(Collectors.joining(";")));
|
||||
}
|
||||
|
||||
@Override public boolean supports(String businessType) { return "RELABEL_ORDER".equals(businessType); }
|
||||
|
||||
+5
-5
@@ -27,13 +27,13 @@ public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventory
|
||||
@Param("warehouseId") String warehouseId, @Param("quantity") long quantity);
|
||||
String FREE_STOCK_COLUMNS = "b.id,b.customer_id,b.overseas_warehouse_id,b.sku_code,b.product_name_zh,b.total_quantity,b.carton_quantity,b.created_at,b.updated_at," +
|
||||
"LEAST(b.available_quantity,COALESCE((SELECT SUM(GREATEST(0,i.quantity-COALESCE(i.packing_reserved_quantity,0))) " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " +
|
||||
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id AND i.sku_code=b.sku_code " +
|
||||
"FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id COLLATE utf8mb4_0900_ai_ci=c.customer_id COLLATE utf8mb4_0900_ai_ci " +
|
||||
"WHERE c.customer_id COLLATE utf8mb4_0900_ai_ci=b.customer_id COLLATE utf8mb4_0900_ai_ci AND c.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci=b.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci AND i.sku_code COLLATE utf8mb4_0900_ai_ci=b.sku_code COLLATE utf8mb4_0900_ai_ci " +
|
||||
"AND c.status='AVAILABLE' AND c.count_status='CONFIRMED'),0)) AS available_quantity," +
|
||||
"COALESCE((SELECT SUM(i.quantity) FROM ll_return_inventory_carton c " +
|
||||
"JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id=c.customer_id " +
|
||||
"WHERE c.customer_id=b.customer_id AND c.overseas_warehouse_id=b.overseas_warehouse_id " +
|
||||
"AND i.sku_code=b.sku_code AND c.status='RELABEL_PENDING'),0) AS relabel_locked_quantity";
|
||||
"JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id AND i.customer_id COLLATE utf8mb4_0900_ai_ci=c.customer_id COLLATE utf8mb4_0900_ai_ci " +
|
||||
"WHERE c.customer_id COLLATE utf8mb4_0900_ai_ci=b.customer_id COLLATE utf8mb4_0900_ai_ci AND c.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci=b.overseas_warehouse_id COLLATE utf8mb4_0900_ai_ci " +
|
||||
"AND i.sku_code COLLATE utf8mb4_0900_ai_ci=b.sku_code COLLATE utf8mb4_0900_ai_ci AND c.status='RELABEL_PENDING'),0) AS relabel_locked_quantity";
|
||||
@Select("SELECT * FROM ll_return_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{warehouseId} AND sku_code=#{skuCode} FOR UPDATE")
|
||||
ReturnInventoryBalance lockSku(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId,
|
||||
|
||||
+2
-2
@@ -72,8 +72,8 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
|
||||
"OR ((SELECT c.relabel_application_id FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) IS NULL " +
|
||||
"AND NOT EXISTS(SELECT 1 FROM ll_relabel_item newer JOIN ll_relabel_application na ON na.id=newer.application_id " +
|
||||
"WHERE newer.return_carton_id=ri.return_carton_id AND na.status NOT IN ('CANCELLED','REJECTED') " +
|
||||
"AND (COALESCE(newer.source_carton_mark,newer.carton_mark)=COALESCE(ri.source_carton_mark,ri.carton_mark) " +
|
||||
"OR COALESCE(newer.source_carton_mark,newer.carton_mark)=(SELECT c.carton_mark FROM ll_return_inventory_carton c WHERE c.id=#{cartonId})) " +
|
||||
"AND (COALESCE(newer.source_carton_mark,newer.carton_mark) COLLATE utf8mb4_0900_ai_ci=COALESCE(ri.source_carton_mark,ri.carton_mark) COLLATE utf8mb4_0900_ai_ci " +
|
||||
"OR COALESCE(newer.source_carton_mark,newer.carton_mark) COLLATE utf8mb4_0900_ai_ci=(SELECT c.carton_mark FROM ll_return_inventory_carton c WHERE c.id=#{cartonId}) COLLATE utf8mb4_0900_ai_ci) " +
|
||||
"AND (na.created_at>ra.created_at OR (na.created_at=ra.created_at AND na.id>ra.id)))))";
|
||||
String ALL_COMPLETION_DEDUCTION = "COALESCE((SELECT SUM(j.quantity) FROM ll_relabel_pda_history h " +
|
||||
"JOIN JSON_TABLE(CASE WHEN h.action='RETURN_COMPLETION_TRANSFER' AND JSON_VALID(h.detail_json) THEN h.detail_json ELSE '{}' END,'$.items[*]' COLUMNS(sku_code VARCHAR(128) PATH '$.skuCode',quantity BIGINT PATH '$.quantity')) j " +
|
||||
|
||||
Reference in New Issue
Block a user