提交
This commit is contained in:
+20
@@ -14,6 +14,26 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
|
||||
@Select("""
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM ll_inbound_plan_carton c
|
||||
JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
||||
JOIN ll_inbound_order i ON i.customer_id=o.customer_id AND i.inbound_code=o.inbound_code
|
||||
WHERE c.customer_id=#{customerId} AND i.id=#{inboundOrderId}
|
||||
AND o.overseas_warehouse_id=#{warehouseId} AND c.carton_mark=#{cartonMark}
|
||||
UNION ALL
|
||||
SELECT 1 FROM ll_warehouse_transfer_carton c
|
||||
JOIN ll_warehouse_transfer_order t ON t.id=c.transfer_order_id AND t.customer_id=c.customer_id
|
||||
WHERE c.customer_id=#{customerId} AND t.inbound_order_id=#{inboundOrderId}
|
||||
AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
|
||||
AND (c.carton_mark=#{cartonMark} OR c.customer_carton_mark=#{cartonMark})
|
||||
)
|
||||
""")
|
||||
boolean cartonBelongsToInbound(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("inboundOrderId") Long inboundOrderId,
|
||||
@Param("cartonMark") String cartonMark);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{overseasWarehouseId} " +
|
||||
"AND destination_warehouse_code=#{destinationWarehouseCode} " +
|
||||
|
||||
+9
-2
@@ -34,6 +34,8 @@ public final class PalletOperationRequests {
|
||||
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
public record MeasuredCartonBinding(@NotBlank @Size(max=255) String cartonMark,
|
||||
@NotBlank @Size(max=255) String sourceCartonMark) {}
|
||||
/** source为空时表示退货箱没有来源托盘,现场仅扫描箱唛后绑定目标托盘;actualSource为现场确认的原入仓库存行。 */
|
||||
public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@NotBlank @Size(max = 64) String relabelNo,
|
||||
@@ -41,10 +43,15 @@ public final class PalletOperationRequests {
|
||||
@Valid PalletExpectation target,
|
||||
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> cartonMarks,
|
||||
@Size(max = 500) String reason,
|
||||
@Valid MoveLine actualSource) {
|
||||
@Valid MoveLine actualSource,
|
||||
@Size(max=500) List<@Valid MeasuredCartonBinding> measuredCartons) {
|
||||
public RelabelDisassemble(String idempotencyKey, String relabelNo, PalletExpectation source,
|
||||
PalletExpectation target, List<String> cartonMarks, String reason, MoveLine actualSource) {
|
||||
this(idempotencyKey, relabelNo, source, target, cartonMarks, reason, actualSource, null);
|
||||
}
|
||||
public RelabelDisassemble(String idempotencyKey, String relabelNo, PalletExpectation source,
|
||||
PalletExpectation target, List<String> cartonMarks, String reason) {
|
||||
this(idempotencyKey, relabelNo, source, target, cartonMarks, reason, null);
|
||||
this(idempotencyKey, relabelNo, source, target, cartonMarks, reason, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-3
@@ -68,6 +68,7 @@ public class PdaPalletOperationService {
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelPalletMutationService relabelPalletMutations;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelCartonMeasurementService cartonMeasurements;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Resource private ReturnInventoryBalanceMapper returnBalances;
|
||||
@@ -307,7 +308,7 @@ public class PdaPalletOperationService {
|
||||
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);
|
||||
recordRelabelBinding(identity, new RelabelMutation(application, byPosition, Map.of(), request.measuredCartons()), retained);
|
||||
}
|
||||
var result = new PalletOperationResponses.Operation(IdWorker.getId(), "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(),
|
||||
"COMPLETED", false, LocalDateTime.now(), List.of(content(identity.overseasWarehouseId(), source)));
|
||||
@@ -351,7 +352,7 @@ public class PdaPalletOperationService {
|
||||
requireRelabelDestination(identity.overseasWarehouseId(), selected, target);
|
||||
Plan base = plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false);
|
||||
Plan relabelPlan = new Plan(base.pallets(), base.moves(), base.changes(),
|
||||
new RelabelMutation(application, itemsByPosition, actual == null ? Map.of() : anchors));
|
||||
new RelabelMutation(application, itemsByPosition, actual == null ? Map.of() : anchors, request.measuredCartons()));
|
||||
String reason = "换标拆托" + (request.reason() == null || request.reason().isBlank()
|
||||
? "" : ":" + request.reason().trim());
|
||||
return execute(identity, "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(), request,
|
||||
@@ -840,7 +841,12 @@ public class PdaPalletOperationService {
|
||||
record.setFromStatus(task.getStatus()); record.setToStatus(task.getStatus());
|
||||
record.setOperatorKey(identity.operatorKey()); record.setOperatorName(identity.operatorName());
|
||||
record.setCreatedAt(LocalDateTime.now());
|
||||
var measured = cartonMeasurements.prepare(app,
|
||||
mutation.itemsByPosition().values().stream().flatMap(List::stream).toList(),
|
||||
mutation.itemsByPosition().keySet().stream().map(positions::selectById).filter(Objects::nonNull).findFirst().orElseThrow().getPalletId(),
|
||||
targets.values().iterator().next().getPalletId(), targets.values().iterator().next().getPalletNo(), mutation.measuredCartons());
|
||||
record.setDetailJson(write(Map.of("round", Objects.toString(task.getReviewedAt(), "INITIAL"),
|
||||
"measuredCartons", measured,
|
||||
"cartonMarks", mutation.itemsByPosition().values().stream().flatMap(List::stream).map(RelabelItem::getCartonMark).toList(),
|
||||
"targetPallets", targets.values().stream().map(InventoryPosition::getPalletNo).distinct().toList(),
|
||||
"sourcePallets", mutation.itemsByPosition().keySet().stream().map(positions::selectById)
|
||||
@@ -1122,6 +1128,11 @@ public class PdaPalletOperationService {
|
||||
RelabelMutation relabelMutation) { }
|
||||
private record RelabelMutation(RelabelApplication application,
|
||||
Map<Long, List<RelabelItem>> itemsByPosition,
|
||||
Map<Long, InventoryPosition> anchors) { }
|
||||
Map<Long, InventoryPosition> anchors,
|
||||
List<com.baiyue.lastleg.palletops.dto.PalletOperationRequests.MeasuredCartonBinding> measuredCartons) {
|
||||
RelabelMutation(RelabelApplication application, Map<Long,List<RelabelItem>> itemsByPosition, Map<Long,InventoryPosition> anchors) {
|
||||
this(application,itemsByPosition,anchors,null);
|
||||
}
|
||||
}
|
||||
private enum InventoryBucket { AVAILABLE, HOLD }
|
||||
}
|
||||
|
||||
+17
@@ -9,6 +9,23 @@ 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) {}
|
||||
record CartonSpec(Long palletId, BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
BigDecimal grossWeightKg, long cartons) {}
|
||||
record Carton(Long id, String cartonMark, String customerCartonMark, Long palletId, String palletCode,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, BigDecimal grossWeightKg) {}
|
||||
@Select("SELECT c.id,c.carton_mark cartonMark,c.customer_carton_mark customerCartonMark,p.warehouse_pallet_id palletId,p.pallet_no palletCode," +
|
||||
"c.length_cm lengthCm,c.width_cm widthCm,c.height_cm heightCm,c.gross_weight_kg grossWeightKg " +
|
||||
"FROM ll_warehouse_transfer_carton c JOIN ll_warehouse_transfer_pallet p " +
|
||||
"ON p.id=c.transfer_pallet_id AND p.customer_id=c.customer_id AND p.transfer_order_id=c.transfer_order_id " +
|
||||
"WHERE c.customer_id=#{customerId} AND c.transfer_order_id=#{transferId} AND c.status='COUNTED' AND p.status='COMPLETED' ORDER BY c.id")
|
||||
List<Carton> cartons(@Param("customerId") String customerId, @Param("transferId") Long transferId);
|
||||
@Select("SELECT p.warehouse_pallet_id palletId,c.length_cm lengthCm,c.width_cm widthCm,c.height_cm heightCm," +
|
||||
"c.gross_weight_kg grossWeightKg,COUNT(*) cartons FROM ll_warehouse_transfer_carton c " +
|
||||
"JOIN ll_warehouse_transfer_pallet p ON p.id=c.transfer_pallet_id AND p.customer_id=c.customer_id AND p.transfer_order_id=c.transfer_order_id " +
|
||||
"WHERE c.customer_id=#{customerId} AND c.transfer_order_id=#{transferId} AND c.status='COUNTED' AND p.status='COMPLETED' " +
|
||||
"GROUP BY p.warehouse_pallet_id,c.length_cm,c.width_cm,c.height_cm,c.gross_weight_kg " +
|
||||
"ORDER BY p.warehouse_pallet_id,c.length_cm,c.width_cm,c.height_cm,c.gross_weight_kg")
|
||||
List<CartonSpec> cartonSpecs(@Param("customerId") String customerId, @Param("transferId") Long transferId);
|
||||
@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," +
|
||||
|
||||
+9
-4
@@ -80,6 +80,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
READY_FOR_OUTBOUND, "COMPLETED", "REJECTED", "CANCELLED");
|
||||
private static final Set<String> DELIVERY_METHODS = Set.of("TRUCK", "EXPRESS", "AMAZON_PICKUP", "CUSTOMER_PICKUP", "PICKUP");
|
||||
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private RelabelApplicationMapper applications;
|
||||
@Resource private RelabelItemMapper items;
|
||||
@Resource private RelabelStatusHistoryMapper histories;
|
||||
@@ -417,13 +418,13 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !request.operationWarehouseId().trim().equals(carton.getOverseasWarehouseId())) {
|
||||
throw conflict("所选退货箱不属于当前客户和操作仓");
|
||||
throw conflict("当前箱子不存在或不属于当前订单");
|
||||
}
|
||||
if (!"AVAILABLE".equals(carton.getStatus()) || carton.getRelabelApplicationId() != null) {
|
||||
throw conflict("仓库唛头 " + carton.getCartonMark() + " 已不可用于换标");
|
||||
}
|
||||
if (!carton.getCartonMark().equals(item.cartonMark().trim()))
|
||||
throw conflict("来源退货箱号与选择的库存不一致");
|
||||
throw conflict("当前箱子不存在或不属于当前订单");
|
||||
com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountGuard.requireConfirmed(carton);
|
||||
final long sourceReturnCartonId = carton.getId();
|
||||
Map<String, Long> availableSku = returnCartonItems.lockCarton(sourceReturnCartonId).stream()
|
||||
@@ -474,6 +475,10 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|| !Objects.equals(balance.getInboundOrderId(), item.sourceInboundOrderId())) {
|
||||
throw conflict("Selected inventory does not belong to this customer, operation warehouse and inbound order");
|
||||
}
|
||||
if (!inventoryBalances.cartonBelongsToInbound(customerId, request.operationWarehouseId().trim(),
|
||||
item.sourceInboundOrderId(), item.cartonMark().trim())) {
|
||||
throw conflict("当前箱子不存在或不属于当前订单");
|
||||
}
|
||||
if (value(position.getAvailablePackages()) < item.notifiedPackageCount()) {
|
||||
throw conflict("Available inventory changed for inbound " + balance.getInboundOrderNo());
|
||||
}
|
||||
@@ -482,7 +487,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long applicationId = IdWorker.getId();
|
||||
String operationWarehouseId = request.operationWarehouseId().trim();
|
||||
String relabelNo = orderNoGenerator.next("RELABEL", operationWarehouseId, applicationId);
|
||||
String relabelNo = compactNumbers.next("TB", customerId);
|
||||
BigDecimal chargeAmount = Objects.requireNonNullElse(request.chargeAmount(), BigDecimal.ZERO);
|
||||
boolean requiresWallet = chargeAmount.signum() > 0;
|
||||
int holdIndex = 0;
|
||||
@@ -1362,7 +1367,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setOrderNo(orderNoGenerator.next("RELABEL", warehouseId, order.getId()));
|
||||
order.setOrderNo(relabelNo);
|
||||
order.setCustomerId(customerId); order.setOrderType("RELABEL_APPLICATION");
|
||||
order.setInboundCode(null); order.setSourceRegion("CUSTOMER");
|
||||
order.setSourceInboundId(relabelNo + ":" + warehouseId);
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baiyue.lastleg.relabel.entity.*;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelPalletMeasurementMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.*;
|
||||
|
||||
/** Replays saved physical box bindings; dimensions always come from measured source boxes. */
|
||||
@Service
|
||||
public class RelabelCartonMeasurementService {
|
||||
@Resource private RelabelPalletMeasurementMapper mapper;
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private ObjectMapper json;
|
||||
public record Move(String cartonMark, String sourceCartonMark, Long targetPalletId, String targetPalletCode) {}
|
||||
|
||||
public List<RelabelPalletMeasurementMapper.Carton> current(RelabelApplication app) {
|
||||
Long transfer = mapper.transfer(app.getCustomerId(), app.getRelabelNo());
|
||||
if (transfer == null) return List.of();
|
||||
var boxes = new LinkedHashMap<String, RelabelPalletMeasurementMapper.Carton>();
|
||||
for (var box : mapper.cartons(app.getCustomerId(), transfer)) boxes.put(box.cartonMark(), box);
|
||||
if (boxes.isEmpty()) return List.of();
|
||||
for (var history : records(app)) if ("PALLET_BIND".equals(history.getAction())) {
|
||||
try {
|
||||
for (var node : json.readTree(history.getDetailJson()).path("measuredCartons")) {
|
||||
var move = json.treeToValue(node, Move.class);
|
||||
var box = boxes.get(move.sourceCartonMark());
|
||||
if (box == null) throw conflict("换标箱测量来源无法追溯");
|
||||
boxes.put(box.cartonMark(), new RelabelPalletMeasurementMapper.Carton(box.id(), box.cartonMark(), box.customerCartonMark(),
|
||||
move.targetPalletId(), move.targetPalletCode(), box.lengthCm(), box.widthCm(), box.heightCm(), box.grossWeightKg()));
|
||||
}
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException e) { throw conflict("换标箱绑定记录无法读取"); }
|
||||
}
|
||||
return List.copyOf(boxes.values());
|
||||
}
|
||||
|
||||
public List<Move> prepare(RelabelApplication app, List<RelabelItem> items, Long sourcePallet,
|
||||
Long targetPallet, String targetCode, List<com.baiyue.lastleg.palletops.dto.PalletOperationRequests.MeasuredCartonBinding> requested) {
|
||||
var boxes = current(app);
|
||||
if (boxes.isEmpty()) return List.of();
|
||||
var available = new ArrayList<>(boxes.stream().filter(box -> sourcePallet.equals(box.palletId())).toList());
|
||||
var mappings = new HashMap<String,String>();
|
||||
// A rebind retains the exact physical box selected at its first binding.
|
||||
for (var history : records(app)) if ("PALLET_BIND".equals(history.getAction())) {
|
||||
try { for (var node : json.readTree(history.getDetailJson()).path("measuredCartons"))
|
||||
mappings.put(node.path("cartonMark").asText(), node.path("sourceCartonMark").asText());
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException e) { throw conflict("换标箱绑定记录无法读取"); }
|
||||
}
|
||||
var explicit = new HashMap<String,String>();
|
||||
var selectedMarks = items.stream().map(RelabelItem::getCartonMark).toList();
|
||||
available.removeIf(box -> mappings.entrySet().stream().anyMatch(entry -> box.cartonMark().equals(entry.getValue()) && !selectedMarks.contains(entry.getKey())));
|
||||
for (var binding : requested == null ? List.<com.baiyue.lastleg.palletops.dto.PalletOperationRequests.MeasuredCartonBinding>of() : requested) {
|
||||
if (binding == null || binding.cartonMark() == null || binding.sourceCartonMark() == null
|
||||
|| items.stream().noneMatch(item -> binding.cartonMark().equals(item.getCartonMark()))
|
||||
|| explicit.put(binding.cartonMark(),binding.sourceCartonMark()) != null)
|
||||
throw conflict("实测箱选择无效或重复");
|
||||
}
|
||||
var result = new ArrayList<Move>();
|
||||
for (var item : items) {
|
||||
if (!Integer.valueOf(1).equals(item.getNotifiedPackageCount())) throw conflict("按箱测量的换标明细必须对应一个实测箱");
|
||||
String prior = mappings.get(item.getCartonMark()), chosen = explicit.get(item.getCartonMark());
|
||||
if (prior != null && chosen != null && !prior.equals(chosen)) throw conflict("改绑托盘不能替换已绑定的实测箱");
|
||||
if (chosen == null) chosen = prior;
|
||||
final String key = chosen;
|
||||
var matched = available.stream().filter(box -> key != null ? key.equals(box.cartonMark())
|
||||
: item.getCartonMark().equals(box.cartonMark()) || item.getCartonMark().equals(box.customerCartonMark())).toList();
|
||||
RelabelPalletMeasurementMapper.Carton box = matched.size() == 1 ? matched.get(0) : null;
|
||||
// Identical measurements may be allocated without a further scan; mixed specifications require an explicit choice.
|
||||
if (box == null && chosen == null && !available.isEmpty() && available.stream().allMatch(b -> sameSpec(available.get(0), b))) box = available.get(0);
|
||||
if (box == null) throw conflict("请选择本次换标对应的来源实测箱,箱规不同不能自动分摊体积和重量");
|
||||
if (!positive(box.lengthCm()) || !positive(box.widthCm()) || !positive(box.heightCm()) || !positive(box.grossWeightKg()))
|
||||
throw conflict("来源箱缺少有效实测尺寸或重量,请先完成箱测量");
|
||||
available.remove(box);
|
||||
result.add(new Move(item.getCartonMark(),box.cartonMark(),targetPallet,targetCode));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private boolean sameSpec(RelabelPalletMeasurementMapper.Carton a, RelabelPalletMeasurementMapper.Carton b) {
|
||||
return equal(a.lengthCm(),b.lengthCm()) && equal(a.widthCm(),b.widthCm()) && equal(a.heightCm(),b.heightCm()) && equal(a.grossWeightKg(),b.grossWeightKg());
|
||||
}
|
||||
private boolean equal(java.math.BigDecimal a, java.math.BigDecimal b) { return a != null && b != null && a.compareTo(b) == 0; }
|
||||
private boolean positive(java.math.BigDecimal value) { return value != null && value.signum() > 0; }
|
||||
private List<PdaRelabelHistory> records(RelabelApplication app) {
|
||||
return histories.selectList(new QueryWrapper<PdaRelabelHistory>().eq("customer_id",app.getCustomerId())
|
||||
.eq("application_id",app.getId()).orderByAsc("created_at","id"));
|
||||
}
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT,message); }
|
||||
}
|
||||
+51
-4
@@ -27,6 +27,7 @@ public class RelabelPalletMeasurementService {
|
||||
@Resource private PdaRelabelHistoryMapper histories;
|
||||
@Resource private PdaRelabelTaskMapper tasks;
|
||||
@Resource private ObjectMapper json;
|
||||
@Resource private RelabelCartonMeasurementService cartonMeasurements;
|
||||
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,
|
||||
@@ -35,7 +36,15 @@ public class RelabelPalletMeasurementService {
|
||||
@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) {}
|
||||
BigDecimal volumeCbm, boolean inherited, List<RelabelPalletMeasurementMapper.CartonSpec> sourceCartonSpecs,
|
||||
boolean cartonAggregated, List<RelabelPalletMeasurementMapper.Carton> cartons) {
|
||||
public Measurement(Long palletId, String palletCode, long packages, String stockVersion,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm, BigDecimal grossWeightKg,
|
||||
BigDecimal volumeCbm, boolean inherited) {
|
||||
this(palletId, palletCode, packages, stockVersion, lengthCm, widthCm, heightCm,
|
||||
grossWeightKg, volumeCbm, inherited, List.of(), false, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public List<Measurement> list(PdaWarehouseIdentity identity, String relabelNo) {
|
||||
return snapshots(scoped(identity, relabelNo, false));
|
||||
@@ -70,6 +79,7 @@ public class RelabelPalletMeasurementService {
|
||||
var current = snapshots(app).stream().filter(p -> p.palletCode().equals(request.palletCode())).findFirst()
|
||||
.orElseThrow(() -> conflict("托盘不属于本换标单或已经清空"));
|
||||
if (!current.stockVersion().equals(request.stockVersion())) throw conflict("托盘库存已变化,请刷新后重新测量");
|
||||
if (current.cartonAggregated()) 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);
|
||||
@@ -85,23 +95,40 @@ public class RelabelPalletMeasurementService {
|
||||
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 cartonSpecs = mapper.cartonSpecs(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 currentBoxes = cartonMeasurements.current(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()));
|
||||
currentBoxes.forEach(box -> involved.add(box.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(","));
|
||||
if (!currentBoxes.isEmpty()) {
|
||||
var boxes = currentBoxes.stream().filter(box -> pallet.equals(box.palletId())).toList();
|
||||
boolean complete = boxes.size() == count && boxes.stream().allMatch(box -> positive(box.lengthCm())
|
||||
&& positive(box.widthCm()) && positive(box.heightCm()) && positive(box.grossWeightKg()));
|
||||
var specs = new LinkedHashMap<List<BigDecimal>,Long>();
|
||||
if (complete) for (var box : boxes) specs.merge(List.of(box.lengthCm(),box.widthCm(),box.heightCm(),box.grossWeightKg()),1L,Long::sum);
|
||||
var grouped = specs.entrySet().stream().map(entry -> new RelabelPalletMeasurementMapper.CartonSpec(pallet,
|
||||
entry.getKey().get(0),entry.getKey().get(1),entry.getKey().get(2),entry.getKey().get(3),entry.getValue())).toList();
|
||||
BigDecimal volume = complete ? boxes.stream().map(box -> box.lengthCm().multiply(box.widthCm()).multiply(box.heightCm()).movePointLeft(6))
|
||||
.reduce(BigDecimal.ZERO,BigDecimal::add).setScale(6,RoundingMode.HALF_UP) : null;
|
||||
BigDecimal weight = complete ? boxes.stream().map(RelabelPalletMeasurementMapper.Carton::grossWeightKg).reduce(BigDecimal.ZERO,BigDecimal::add) : null;
|
||||
result.add(new Measurement(pallet,rows.get(0).palletNo(),count,version,null,null,null,weight,volume,true,grouped,true,boxes));
|
||||
continue;
|
||||
}
|
||||
Measurement value = null;
|
||||
for (var h : records) if ("PALLET_MEASURE".equals(h.getAction())) {
|
||||
var measured = json.convertValue(tree(h).path("measurement"), Measurement.class);
|
||||
@@ -110,7 +137,9 @@ public class RelabelPalletMeasurementService {
|
||||
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);
|
||||
positiveOrNull(original.lengthCm()), positiveOrNull(original.widthCm()), positiveOrNull(original.heightCm()),
|
||||
original.grossWeightKg(), original.volumeCbm(), true,
|
||||
cartonSpecs.stream().filter(spec -> pallet.equals(spec.palletId())).toList(), false, List.of());
|
||||
}
|
||||
result.add(value == null ? new Measurement(pallet, rows.get(0).palletNo(), count, version, null,null,null,null,null,false) : value);
|
||||
}
|
||||
@@ -118,10 +147,28 @@ public class RelabelPalletMeasurementService {
|
||||
}
|
||||
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
|
||||
|| !(positive(p.lengthCm()) && positive(p.widthCm()) && positive(p.heightCm())) && !completeSourceCartons(p)
|
||||
|| p.grossWeightKg().signum() <= 0 || p.volumeCbm().signum() <= 0)
|
||||
throw conflict("托盘 " + p.palletCode() + " 拆合托后尚未确认实测尺寸和重量,请在 PDA 补测(含原托盘剩余货物)");
|
||||
throw conflict("托盘 " + p.palletCode() + (p.cartonAggregated()
|
||||
? " 实测箱数据不完整或与库存箱数不一致,请刷新核对来源箱"
|
||||
: " 拆合托后尚未确认实测尺寸和重量,请在 PDA 补测(含原托盘剩余货物)"));
|
||||
}
|
||||
private boolean completeSourceCartons(Measurement p) {
|
||||
if (!p.inherited() || p.sourceCartonSpecs() == null || p.sourceCartonSpecs().isEmpty()) return false;
|
||||
long count = 0; BigDecimal volume = BigDecimal.ZERO, weight = BigDecimal.ZERO;
|
||||
for (var spec : p.sourceCartonSpecs()) {
|
||||
if (spec.cartons() <= 0 || !positive(spec.lengthCm()) || !positive(spec.widthCm())
|
||||
|| !positive(spec.heightCm()) || !positive(spec.grossWeightKg())) return false;
|
||||
count += spec.cartons();
|
||||
var boxes = BigDecimal.valueOf(spec.cartons());
|
||||
volume = volume.add(spec.lengthCm().multiply(spec.widthCm()).multiply(spec.heightCm()).movePointLeft(6).multiply(boxes));
|
||||
weight = weight.add(spec.grossWeightKg().multiply(boxes));
|
||||
}
|
||||
return count == p.packages() && volume.setScale(6, RoundingMode.HALF_UP).compareTo(p.volumeCbm()) == 0
|
||||
&& weight.compareTo(p.grossWeightKg()) == 0;
|
||||
}
|
||||
private static boolean positive(BigDecimal value) { return value != null && value.signum() > 0; }
|
||||
private static BigDecimal positiveOrNull(BigDecimal value) { return positive(value) ? value : null; }
|
||||
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"));
|
||||
|
||||
+4
-3
@@ -537,7 +537,7 @@ public class ReturnInventoryService {
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
// One durable barcode identifies a physical parcel, irrespective of SKU count.
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "RP" + hash(List.of(receipt.getId())).substring(0, 24).toUpperCase(java.util.Locale.ROOT);
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "RP-" + Long.toString(receipt.getId(), 36).toUpperCase(java.util.Locale.ROOT);
|
||||
else if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
if (cartons.findMark(cartonMark) != null) throw conflict("箱号已存在: " + cartonMark);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -602,7 +602,8 @@ public class ReturnInventoryService {
|
||||
}
|
||||
|
||||
private String shortReturnMark(String returnNo, Long receiptId, String skuCode, long totalBoxes, long sequence) {
|
||||
String prefix = hash(java.util.Arrays.asList(returnNo, receiptId, skuCode)).substring(0, 16).toUpperCase(java.util.Locale.ROOT);
|
||||
String prefix = new java.math.BigInteger(hash(java.util.Arrays.asList(returnNo, receiptId, skuCode)).substring(0, 16), 16)
|
||||
.toString(36).toUpperCase(java.util.Locale.ROOT);
|
||||
return "RC" + prefix + "#" + totalBoxes + "-" + sequence;
|
||||
}
|
||||
|
||||
@@ -1276,7 +1277,7 @@ public class ReturnInventoryService {
|
||||
.map(ReturnInventoryRequests.PlanItem::skuCode).distinct().sorted().toList()).toList();
|
||||
Map<List<String>, Long> totals = groups.stream().collect(java.util.stream.Collectors.groupingBy(group -> group, LinkedHashMap::new, java.util.stream.Collectors.counting()));
|
||||
Map<List<String>, String> prefixes = new HashMap<>(); Map<List<String>, Long> sequences = new HashMap<>();
|
||||
return groups.stream().map(group -> prefixes.computeIfAbsent(group, ignored -> "RC" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase(java.util.Locale.ROOT))
|
||||
return groups.stream().map(group -> prefixes.computeIfAbsent(group, ignored -> LastlegOrderNoGenerator.compactCartonMark(IdWorker.getId()))
|
||||
+ "#" + totals.get(group) + "-" + sequences.merge(group, 1L, Long::sum)).toList();
|
||||
}
|
||||
private String hash(Object request) { try { return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
|
||||
+6
-5
@@ -62,6 +62,7 @@ public class ReturnReceiptService {
|
||||
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private ReturnOrderMapper orders;
|
||||
@Resource private ReturnOrderItemMapper orderItems;
|
||||
@Resource private ReturnReceiptMapper receipts;
|
||||
@@ -99,7 +100,7 @@ public class ReturnReceiptService {
|
||||
order.setOverseasWarehouseId(text(request.overseasWarehouseId()));
|
||||
order.setWarehouseContactName(text(request.warehouseContactName()));
|
||||
order.setWarehouseContactPhone(text(request.warehouseContactPhone()));
|
||||
order.setReturnNo(numbers.next("RETURN", order.getOverseasWarehouseId(), order.getId()));
|
||||
order.setReturnNo(compactNumbers.next("TT", customerId));
|
||||
order.setCustomerReferenceNo(text(request.customerReferenceNo())); order.setNeedRelabel(request.needRelabel()); order.setStatus(PENDING_REVIEW);
|
||||
order.setExpectedSkuTypes(skuCodes.size()); order.setExpectedQuantity(expected); order.setReceivedQuantity(0L);
|
||||
order.setRemark(text(request.remark())); order.setIdempotencyKey(key); order.setRequestHash(requestHash);
|
||||
@@ -429,10 +430,10 @@ public class ReturnReceiptService {
|
||||
totalQuantity = Math.addExact(totalQuantity, requested.quantity());
|
||||
}
|
||||
long appId = IdWorker.getId(); LocalDateTime now = LocalDateTime.now();
|
||||
String relabelNo = numbers.next("RELABEL", order.getOverseasWarehouseId(), appId);
|
||||
String relabelNo = compactNumbers.next("TT", customerId);
|
||||
LastlegOrder tail = order.getLastlegOrderId() == null ? null : lastlegOrders.selectById(order.getLastlegOrderId());
|
||||
if (tail == null) {
|
||||
tail = new LastlegOrder(); tail.setId(IdWorker.getId()); tail.setOrderNo(numbers.next("RELABEL", order.getOverseasWarehouseId(), tail.getId()));
|
||||
tail = new LastlegOrder(); tail.setId(IdWorker.getId()); tail.setOrderNo(relabelNo);
|
||||
tail.setCustomerId(customerId); tail.setOrderType("RELABEL_APPLICATION"); tail.setSourceRegion("CUSTOMER");
|
||||
tail.setSourceInboundId("RETURN_RELABEL:" + relabelNo);
|
||||
tail.setSourceEventId("RETURN_RELABEL:" + relabelNo);
|
||||
@@ -612,9 +613,9 @@ public class ReturnReceiptService {
|
||||
if ("PARCEL".equals(unitType) && request.totalBoxCount() != null) throw badRequest("包裹签收不填写箱数");
|
||||
if (unitCode == null && "PALLET".equals(unitType)) unitCode = text(request.palletCode());
|
||||
if (unitCode == null && "BOX".equals(unitType))
|
||||
unitCode = "RC" + hash(List.of(order.getReturnNo(), IdWorker.getId())).substring(0, 16).toUpperCase(java.util.Locale.ROOT) + "#1-1";
|
||||
unitCode = LastlegOrderNoGenerator.compactCartonMark(IdWorker.getId());
|
||||
if (unitCode == null && "PARCEL".equals(unitType)) {
|
||||
unitCode = "PK" + hash(List.of(order.getReturnNo(), IdWorker.getId())).substring(0, 16).toUpperCase(java.util.Locale.ROOT);
|
||||
unitCode = "PK-" + Long.toString(IdWorker.getId(), 36).toUpperCase(java.util.Locale.ROOT);
|
||||
}
|
||||
if (unitCode == null) throw badRequest(("PALLET".equals(unitType) ? "托盘码" : "包裹码") + "不能为空");
|
||||
if ("PALLET".equals(unitType)
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Allocates suffix-free document numbers across all service instances. */
|
||||
@Component
|
||||
public class CompactBusinessNumberService {
|
||||
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyMMddHHmmss", Locale.ROOT).withZone(ZONE);
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ReturnOrderReferenceResolver customers;
|
||||
|
||||
public CompactBusinessNumberService(JdbcTemplate jdbc, ReturnOrderReferenceResolver customers) {
|
||||
this.jdbc = jdbc;
|
||||
this.customers = customers;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public String next(String prefix, String customerId) {
|
||||
return allocate(prefix, initials(customers.customerName(customerId)), Instant.now());
|
||||
}
|
||||
|
||||
String allocate(String prefix, String initials, Instant now) {
|
||||
String key = prefix + "-" + initials;
|
||||
long seconds = now.getEpochSecond();
|
||||
// The row lock serializes identical prefixes/initials even across customers and JVMs.
|
||||
jdbc.update("INSERT INTO ll_compact_number_clock(number_key,last_second) VALUES(?,LAST_INSERT_ID(?)) "
|
||||
+ "ON DUPLICATE KEY UPDATE last_second=LAST_INSERT_ID(GREATEST(last_second+1,?))", key, seconds, seconds);
|
||||
Long allocated = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
||||
if (allocated == null || allocated < seconds) throw new IllegalStateException("业务编号生成失败");
|
||||
return key + "-" + FORMAT.format(Instant.ofEpochSecond(allocated));
|
||||
}
|
||||
|
||||
static String initials(String name) {
|
||||
if (name == null || name.isBlank()) return "KH";
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean latinWord = false;
|
||||
for (char c : name.trim().toCharArray()) {
|
||||
if (result.length() >= 4) break;
|
||||
if (Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN) {
|
||||
String[] pinyin = PinyinHelper.toHanyuPinyinStringArray(c);
|
||||
if (pinyin != null && pinyin.length > 0) result.append(Character.toUpperCase(pinyin[0].charAt(0)));
|
||||
latinWord = false;
|
||||
} else if (c < 128 && Character.isLetterOrDigit(c)) {
|
||||
if (!latinWord) result.append(Character.toUpperCase(c));
|
||||
latinWord = true;
|
||||
} else latinWord = false;
|
||||
}
|
||||
return result.isEmpty() ? "KH" : result.toString();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public class LastlegOrderNoGenerator {
|
||||
|
||||
public String next(String businessType, String warehouse, long uniqueId) {
|
||||
String code = businessType == null ? "" : businessType.trim().toUpperCase(Locale.ROOT);
|
||||
if ("RETURN_CARTON".equals(code)) return compactCartonMark(uniqueId);
|
||||
String returnPrefix = switch (code) {
|
||||
case "RETURN" -> "RT";
|
||||
case "RETURN_RECEIPT" -> "RR";
|
||||
@@ -65,6 +66,12 @@ public class LastlegOrderNoGenerator {
|
||||
return "0".repeat(25 - encoded.length()) + encoded;
|
||||
}
|
||||
|
||||
/** Preserve every snowflake ID bit; do not truncate or hash physical label identifiers. */
|
||||
public static String compactCartonMark(long id) {
|
||||
if (id <= 0) throw new IllegalArgumentException("Carton id must be positive");
|
||||
return "BX-" + Long.toString(id, 36).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeType(String value) {
|
||||
String code = value == null ? "LMO" : value.trim().toUpperCase(Locale.ROOT);
|
||||
return switch (code) {
|
||||
|
||||
+4
@@ -65,6 +65,10 @@ public class ReturnOrderReferenceResolver {
|
||||
}
|
||||
}
|
||||
|
||||
public String customerName(String id) {
|
||||
return customerReference(id).tenantName();
|
||||
}
|
||||
|
||||
private ReturnCustomerReferenceClient.Customer customerReference(String id) {
|
||||
try {
|
||||
var result = customers.customers(internalToken, 1, 30, null, "ACTIVE", Long.parseLong(id));
|
||||
|
||||
+8
@@ -28,6 +28,14 @@ import java.util.List;
|
||||
public class MerchantWarehouseTransferController {
|
||||
@Resource private WarehouseTransferService service;
|
||||
|
||||
@GetMapping("/inventory-source-cartons")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
|
||||
public ApiResponse<List<WarehouseTransferResponses.SourceCarton>> sourceCartons(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam String operatingWarehouseId,
|
||||
@RequestParam String inboundCode) {
|
||||
return ApiResponse.success(service.sourceCartons(customer(user), operatingWarehouseId, inboundCode));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+11
-3
@@ -29,13 +29,13 @@ public final class WarehouseTransferRequests {
|
||||
@Size(max = 32) String privateDestinationPostalCode,
|
||||
@Size(max = 128) String privateDestinationContactName,
|
||||
@Size(max = 64) String privateDestinationContactPhone,
|
||||
@NotBlank @Size(max = 1000) String deliveryAddress,
|
||||
@Size(max = 1000) String deliveryAddress,
|
||||
@Size(max = 128) String pickupContactName,
|
||||
@Size(max = 64) String pickupContactPhone,
|
||||
@Positive int packageCount,
|
||||
@NotNull @DecimalMin(value = "0.0") BigDecimal volumeCbm,
|
||||
@NotBlank @Size(max = 64) String deliveryMethod,
|
||||
@NotNull LocalDateTime estimatedArrivalAt,
|
||||
@Size(max = 64) String deliveryMethod,
|
||||
LocalDateTime estimatedArrivalAt,
|
||||
LocalDateTime estimatedShipAt,
|
||||
@Size(max = 128) String fbaShipmentNo,
|
||||
@Size(max = 255) String trackingNo,
|
||||
@@ -51,6 +51,14 @@ public final class WarehouseTransferRequests {
|
||||
@NotEmpty @Size(max = 500) List<@Valid Item> items,
|
||||
@Size(max = 1000) List<@Valid RelabelItem> relabelItems
|
||||
) {
|
||||
@jakarta.validation.constraints.AssertTrue(message="外来货物须填写入仓方式、地址和预计入仓时间;库存转运须选择FBA或私人仓")
|
||||
public boolean isInboundModeValid() {
|
||||
if (inboundCode != null && !inboundCode.isBlank())
|
||||
return "FBA".equals(destinationType) || "PRIVATE".equals(destinationType)
|
||||
|| destinationType == null && fbaWarehouseCode != null && !fbaWarehouseCode.isBlank();
|
||||
return deliveryMethod != null && !deliveryMethod.isBlank() && deliveryAddress != null
|
||||
&& !deliveryAddress.isBlank() && estimatedArrivalAt != null;
|
||||
}
|
||||
public Create(String inboundCode, List<Long> sourceLastlegOrderIds, String operatingWarehouseId,
|
||||
String fbaWarehouseCode, String deliveryAddress, int packageCount, BigDecimal volumeCbm,
|
||||
String deliveryMethod, LocalDateTime estimatedArrivalAt, String fbaShipmentNo,
|
||||
|
||||
+3
@@ -6,6 +6,9 @@ import java.util.List;
|
||||
|
||||
public final class WarehouseTransferResponses {
|
||||
private WarehouseTransferResponses() {}
|
||||
public record SourceCarton(String cartonMark, String productNameZh, Long quantity,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, List<SourceCartonSku> items) {}
|
||||
public record SourceCartonSku(String skuCode, Long quantity) {}
|
||||
public record Item(Long id, String skuCode, String productNameZh, int packageCount, String shippingMark, Long quantity) {}
|
||||
public record Carton(String cartonMark, String customerCartonMark, int cartonIndex, int totalCartons, String status,
|
||||
Long transferItemId, Long transferPalletId, BigDecimal length, BigDecimal width, BigDecimal height,
|
||||
|
||||
+19
@@ -3,6 +3,25 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
@Mapper public interface InboundSnapshotMapper {
|
||||
record SourceCarton(String cartonMark, String skuCode, String productNameZh, Long quantity,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, String skuItemsJson) {}
|
||||
@Select("""
|
||||
SELECT c.carton_mark cartonMark,c.sku_code skuCode,NULL productNameZh,NULL quantity,
|
||||
c.destination_warehouse_code destinationWarehouseCode,c.destination_warehouse_name destinationWarehouseName,NULL skuItemsJson
|
||||
FROM ll_inbound_plan_carton c JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND o.overseas_warehouse_id=#{warehouseId}
|
||||
UNION ALL
|
||||
SELECT c.carton_mark,i.sku_code,i.product_name_zh,c.counted_quantity,
|
||||
t.destination_warehouse_code,t.destination_warehouse_name,c.sku_items_json
|
||||
FROM ll_warehouse_transfer_carton c
|
||||
JOIN ll_warehouse_transfer_order t ON t.id=c.transfer_order_id AND t.customer_id=c.customer_id
|
||||
JOIN ll_order o ON o.id=t.lastleg_order_id AND o.customer_id=t.customer_id
|
||||
JOIN ll_warehouse_transfer_item i ON i.id=c.transfer_item_id AND i.customer_id=c.customer_id AND i.transfer_order_id=t.id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode} AND t.operating_warehouse_id=#{warehouseId} AND c.status='COUNTED'
|
||||
ORDER BY cartonMark
|
||||
""")
|
||||
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
|
||||
@Param("warehouseId") String warehouseId, @Param("inboundCode") String inboundCode);
|
||||
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND (inbound_code=#{code} OR source_inbound_id=#{code}) LIMIT 1")
|
||||
Long findId(@Param("customerId") String customerId, @Param("code") String code);
|
||||
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND source_inbound_id=#{sourceInboundId} LIMIT 1")
|
||||
|
||||
+62
-17
@@ -88,6 +88,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class WarehouseTransferService {
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge fleetPool;
|
||||
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderQueries fleetPoolQueries;
|
||||
@@ -140,14 +141,35 @@ public class WarehouseTransferService {
|
||||
@Resource private InventoryCommandMapper inventoryCommands;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
public List<WarehouseTransferResponses.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
if (!StringUtils.hasText(warehouseId) || !StringUtils.hasText(inboundCode)) throw badRequest("请选择海外仓和库存来源单据");
|
||||
return inboundSnapshots.sourceCartons(customerId, warehouseId.trim(), inboundCode.trim()).stream().map(row -> {
|
||||
var carton = new WarehouseTransferCarton(); carton.setSkuItemsJson(row.skuItemsJson());
|
||||
var contents = cartonContents(carton).stream().map(item -> new WarehouseTransferResponses.SourceCartonSku(item.skuCode(),item.quantity())).toList();
|
||||
if (contents.isEmpty()) contents = List.of(new WarehouseTransferResponses.SourceCartonSku(row.skuCode(),row.quantity()));
|
||||
return new WarehouseTransferResponses.SourceCarton(row.cartonMark(),row.productNameZh(),row.quantity(),
|
||||
row.destinationWarehouseCode(),row.destinationWarehouseName(),contents);
|
||||
}).toList();
|
||||
}
|
||||
private static LocalDateTime arrivalTime(WarehouseTransferRequests.Create request) {
|
||||
return StringUtils.hasText(request.inboundCode()) ? null : request.estimatedArrivalAt();
|
||||
}
|
||||
private static String inboundMethod(WarehouseTransferRequests.Create request) {
|
||||
return StringUtils.hasText(request.inboundCode()) ? "INVENTORY_TRANSFER" : request.deliveryMethod().trim();
|
||||
}
|
||||
private static String inboundAddress(WarehouseTransferRequests.Create request) {
|
||||
return StringUtils.hasText(request.inboundCode()) ? "" : request.deliveryAddress().trim();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail create(String customerId, String operatorId,
|
||||
WarehouseTransferRequests.Create request) {
|
||||
if (!request.isInboundModeValid()) throw badRequest("外来货物须填写入仓信息,库存转运须选择FBA仓或私人仓");
|
||||
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
|
||||
if (itemPackages != request.packageCount()) {
|
||||
throw badRequest("Cargo item package total must equal transfer package count");
|
||||
}
|
||||
validateRelabelRequest(request);
|
||||
validateRelabelRequest(customerId, request);
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
@@ -167,7 +189,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long transferId = IdWorker.getId();
|
||||
String transferNo = orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), transferId);
|
||||
String transferNo = compactNumbers.next("ZY", customerId);
|
||||
String requestedInboundCode = trim(request.inboundCode());
|
||||
List<Long> selectedIds = request.sourceLastlegOrderIds() == null
|
||||
? List.of() : request.sourceLastlegOrderIds().stream().distinct().toList();
|
||||
@@ -210,13 +232,13 @@ public class WarehouseTransferService {
|
||||
transfer.setOperatingWarehouseId(request.operatingWarehouseId().trim());
|
||||
transfer.setFbaWarehouseCode(destinationCode(request));
|
||||
applyDestination(transfer, request);
|
||||
transfer.setDeliveryAddress(request.deliveryAddress().trim()); transfer.setPackageCount(request.packageCount());
|
||||
transfer.setDeliveryAddress(inboundAddress(request)); transfer.setPackageCount(request.packageCount());
|
||||
transfer.setPickupContactName(trim(request.pickupContactName()));
|
||||
transfer.setPickupContactPhone(trim(request.pickupContactPhone()));
|
||||
transfer.setDeliveryMethod(request.deliveryMethod().trim()); transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
|
||||
transfer.setDeliveryMethod(inboundMethod(request)); transfer.setEstimatedArrivalAt(arrivalTime(request));
|
||||
transfer.setEstimatedShipAt(request.estimatedShipAt());
|
||||
transfer.setFbaShipmentNo(trim(request.fbaShipmentNo()));
|
||||
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim()) ? trim(request.trackingNo()) : null);
|
||||
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(inboundMethod(request)) ? trim(request.trackingNo()) : null);
|
||||
transfer.setFbaBoxNos(trim(request.fbaBoxNos())); transfer.setRemark(trim(request.remark()));
|
||||
transfer.setNeedRelabel(request.needRelabel()); transfer.setStatus(DRAFT);
|
||||
transfer.setLabelDeadline(request.labelDeadline());
|
||||
@@ -278,11 +300,12 @@ public class WarehouseTransferService {
|
||||
if (!DRAFT.equals(transfer.getStatus())) {
|
||||
throw conflict("Only a draft warehouse transfer order can be edited");
|
||||
}
|
||||
if (!request.isInboundModeValid()) throw badRequest("外来货物须填写入仓信息,库存转运须选择FBA仓或私人仓");
|
||||
int itemPackages = request.items().stream().mapToInt(WarehouseTransferRequests.Item::packageCount).sum();
|
||||
if (itemPackages != request.packageCount()) {
|
||||
throw badRequest("Cargo item package total must equal transfer package count");
|
||||
}
|
||||
validateRelabelRequest(request);
|
||||
validateRelabelRequest(customerId, request);
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
@@ -293,15 +316,15 @@ public class WarehouseTransferService {
|
||||
transfer.setOperatingWarehouseId(request.operatingWarehouseId().trim());
|
||||
transfer.setFbaWarehouseCode(destinationCode(request));
|
||||
applyDestination(transfer, request);
|
||||
transfer.setDeliveryAddress(request.deliveryAddress().trim());
|
||||
transfer.setDeliveryAddress(inboundAddress(request));
|
||||
transfer.setPackageCount(request.packageCount());
|
||||
transfer.setPickupContactName(trim(request.pickupContactName()));
|
||||
transfer.setPickupContactPhone(trim(request.pickupContactPhone()));
|
||||
transfer.setDeliveryMethod(request.deliveryMethod().trim());
|
||||
transfer.setEstimatedArrivalAt(request.estimatedArrivalAt());
|
||||
transfer.setDeliveryMethod(inboundMethod(request));
|
||||
transfer.setEstimatedArrivalAt(arrivalTime(request));
|
||||
transfer.setEstimatedShipAt(request.estimatedShipAt());
|
||||
transfer.setFbaShipmentNo(trim(request.fbaShipmentNo()));
|
||||
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim())
|
||||
transfer.setTrackingNo("CUSTOMER_DELIVERY".equals(inboundMethod(request))
|
||||
? trim(request.trackingNo()) : null);
|
||||
transfer.setFbaBoxNos(trim(request.fbaBoxNos()));
|
||||
transfer.setRemark(trim(request.remark()));
|
||||
@@ -319,7 +342,7 @@ public class WarehouseTransferService {
|
||||
order.setFbaWarehouseCode(destinationCode(request));
|
||||
order.setPackageCount(request.packageCount());
|
||||
order.setVolumeCbm(request.volumeCbm());
|
||||
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
|
||||
order.setEstimatedWarehouseAt(arrivalTime(request));
|
||||
order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setCustomerRemark(trim(request.remark()));
|
||||
@@ -376,6 +399,19 @@ public class WarehouseTransferService {
|
||||
if (PENDING_REVIEW.equals(transfer.getStatus())) return detail(customerId, transferNo);
|
||||
if (!DRAFT.equals(transfer.getStatus())) throw conflict("Only a draft warehouse transfer order can be submitted");
|
||||
|
||||
if (Boolean.TRUE.equals(transfer.getNeedRelabel())) {
|
||||
Set<String> allowedMarks = !transfer.getTransferNo().equals(transfer.getInboundCode())
|
||||
? inboundSnapshots.sourceCartons(customerId, transfer.getOperatingWarehouseId(), transfer.getInboundCode())
|
||||
.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark).collect(Collectors.toSet())
|
||||
: transferCartons.listOrder(customerId, transfer.getId()).stream()
|
||||
.flatMap(carton -> java.util.stream.Stream.of(carton.getCartonMark(), carton.getCustomerCartonMark()))
|
||||
.filter(StringUtils::hasText).collect(Collectors.toSet());
|
||||
for (var binding : relabelBindings.listOrder(customerId, transfer.getId())) {
|
||||
if (!allowedMarks.contains(binding.getCustomerCartonMark()))
|
||||
throw badRequest("当前箱子不存在或不属于当前订单");
|
||||
}
|
||||
}
|
||||
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
transfer.setStatus(PENDING_REVIEW);
|
||||
@@ -859,7 +895,7 @@ public class WarehouseTransferService {
|
||||
carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId());
|
||||
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(input.transferItemId()); carton.setCustomerId(customerId);
|
||||
carton.setCartonIndex(++nextIndex); carton.setTotalCartons(transfer.getPackageCount());
|
||||
carton.setCartonMark(transferNo + "#EX-" + nextIndex); carton.setCreatedAt(now);
|
||||
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId())); carton.setCreatedAt(now);
|
||||
}
|
||||
carton.setTransferPalletId(input.transferPalletId());
|
||||
carton.setLengthCm(input.length()); carton.setWidthCm(input.width()); carton.setHeightCm(input.height());
|
||||
@@ -1712,7 +1748,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRelabelRequest(WarehouseTransferRequests.Create request) {
|
||||
private void validateRelabelRequest(String customerId, WarehouseTransferRequests.Create request) {
|
||||
if (!request.needRelabel()) {
|
||||
if (request.relabelItems() != null && !request.relabelItems().isEmpty()) {
|
||||
throw badRequest("Relabel items require needRelabel=true");
|
||||
@@ -1720,6 +1756,14 @@ public class WarehouseTransferService {
|
||||
return;
|
||||
}
|
||||
if (request.relabelItems() == null || request.relabelItems().isEmpty()) return;
|
||||
Set<String> orderCartonMarks = StringUtils.hasText(request.inboundCode())
|
||||
? inboundSnapshots.sourceCartons(customerId, request.operatingWarehouseId().trim(), request.inboundCode().trim())
|
||||
.stream().map(InboundSnapshotMapper.SourceCarton::cartonMark)
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet())
|
||||
: request.items().stream().flatMap(item -> splitMarks(item.shippingMark()).stream()).collect(Collectors.toSet());
|
||||
if (!StringUtils.hasText(request.inboundCode()) && orderCartonMarks.isEmpty()) {
|
||||
orderCartonMarks.addAll(splitMarks(request.fbaBoxNos()));
|
||||
}
|
||||
if (request.labelDeadline() == null || !StringUtils.hasText(request.relabelContactName())
|
||||
|| !StringUtils.hasText(request.relabelContactPhone())) {
|
||||
throw badRequest("Relabel deadline and contact are required");
|
||||
@@ -1754,6 +1798,7 @@ public class WarehouseTransferService {
|
||||
throw badRequest("SKU relabel requires a new SKU");
|
||||
}
|
||||
for (String mark : row.customerCartonMarks()) {
|
||||
if (!orderCartonMarks.contains(mark.trim())) throw badRequest("当前箱子不存在或不属于当前订单");
|
||||
if (!marks.add(mark.trim().toUpperCase())) throw badRequest("Customer carton marks must be unique");
|
||||
}
|
||||
}
|
||||
@@ -1969,7 +2014,7 @@ public class WarehouseTransferService {
|
||||
private LastlegOrder createTailOrder(String customerId, String customerName, Long inboundOrderId, String inboundCode, String transferNo,
|
||||
WarehouseTransferRequests.Create request, String requestHash, LocalDateTime now) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(IdWorker.getId()); order.setOrderNo(orderNoGenerator.next("TRANSFER", request.operatingWarehouseId(), order.getId())); order.setCustomerId(customerId);
|
||||
order.setId(IdWorker.getId()); order.setOrderNo(transferNo); order.setCustomerId(customerId);
|
||||
order.setCustomerName(customerName);
|
||||
order.setOrderType(ORDER_TYPE);
|
||||
order.setInboundCode(inboundCode); order.setSourceRegion("CUSTOMER");
|
||||
@@ -1979,7 +2024,7 @@ public class WarehouseTransferService {
|
||||
order.setVolumeCbm(request.volumeCbm());
|
||||
order.setContainerNo(transferNo);
|
||||
order.setPalletCount(0); order.setPickedPackageCount(0); order.setShippedPackageCount(0);
|
||||
order.setEstimatedWarehouseAt(request.estimatedArrivalAt());
|
||||
order.setEstimatedWarehouseAt(arrivalTime(request));
|
||||
order.setCurrentStatus(DRAFT);
|
||||
order.setRelabelStatus(request.needRelabel() ? "PENDING_BINDING" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + DRAFT + ";RELABEL:" + order.getRelabelStatus());
|
||||
@@ -2007,7 +2052,7 @@ public class WarehouseTransferService {
|
||||
String customerCartonMark, LocalDateTime now) {
|
||||
WarehouseTransferCarton carton = new WarehouseTransferCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setTransferOrderId(transferId); carton.setTransferItemId(transferItemId);
|
||||
carton.setCustomerId(customerId); carton.setCartonMark(transferNo + "#" + totalCartons + "-" + cartonIndex);
|
||||
carton.setCustomerId(customerId); carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId()));
|
||||
carton.setCustomerCartonMark(trim(customerCartonMark));
|
||||
carton.setCartonIndex(cartonIndex); carton.setTotalCartons(totalCartons); carton.setStatus("EXPECTED");
|
||||
carton.setCreatedAt(now); carton.setUpdatedAt(now); transferCartons.insert(carton);
|
||||
@@ -2020,7 +2065,7 @@ public class WarehouseTransferService {
|
||||
detail.setOrderId(orderId); detail.setSourceInboundDetailId(transferNo + ":" + index);
|
||||
detail.setSkuCode(trim(source.skuCode())); detail.setProductNameZh(trim(source.productNameZh()));
|
||||
detail.setPackageCount(source.packageCount()); detail.setDestinationWarehouseCode(destinationCode(request));
|
||||
detail.setFbaTrackingNo("CUSTOMER_DELIVERY".equals(request.deliveryMethod().trim())
|
||||
detail.setFbaTrackingNo("CUSTOMER_DELIVERY".equals(inboundMethod(request))
|
||||
? trim(request.trackingNo()) : null);
|
||||
detail.setShipmentNo(trim(request.fbaShipmentNo()));
|
||||
detail.setShippingMark(trim(source.shippingMark())); orderDetails.insert(detail);
|
||||
|
||||
Reference in New Issue
Block a user