贴标申请,仓储转运
This commit is contained in:
+14
-2
@@ -80,7 +80,13 @@ public final class DeliveryRequests {
|
||||
@Size(max=1000) String remark,
|
||||
@NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=128) String skuCode,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType) {
|
||||
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType,
|
||||
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
|
||||
@jakarta.validation.constraints.Positive Long quantity) {
|
||||
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
|
||||
List<String> cartonMarks, String remark, String idempotencyKey, String skuCode, String unitType) {
|
||||
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, unitType, null);
|
||||
}
|
||||
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
|
||||
List<String> cartonMarks, String remark, String idempotencyKey) {
|
||||
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, null, null);
|
||||
@@ -114,7 +120,13 @@ public final class DeliveryRequests {
|
||||
public record PdaPick(@NotNull Long outboundPalletItemId, Long inventoryPositionId,
|
||||
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey,
|
||||
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks,
|
||||
@Size(max=128) String skuCode) {
|
||||
@Size(max=128) String skuCode,
|
||||
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
|
||||
@jakarta.validation.constraints.Positive Long productQuantity) {
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey,
|
||||
List<String> cartonMarks, String skuCode) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, cartonMarks, skuCode, null);
|
||||
}
|
||||
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey) {
|
||||
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of(), null);
|
||||
}
|
||||
|
||||
+2
-2
@@ -92,7 +92,7 @@ public final class DeliveryResponses {
|
||||
String skuCode, String inboundCode, long availablePackages,
|
||||
long pickedPackages) {}
|
||||
public record PdaTaskItem(Long outboundPalletItemId, String palletNo, String skuCode, String inboundCode,
|
||||
int packageCount, int pickedPackageCount, String status,
|
||||
List<PdaPickPosition> positions) {}
|
||||
int packageCount, int pickedPackageCount, String status,
|
||||
List<PdaPickPosition> positions, Long returnSkuQuantity) {}
|
||||
public record PdaTaskDetail(PdaTask task, List<PdaTaskItem> items) {}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.delivery.dto;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import java.io.IOException;
|
||||
|
||||
/** Piece quantities must not silently truncate a fractional JSON number. */
|
||||
public final class WholePieceQuantityDeserializer extends JsonDeserializer<Long> {
|
||||
@Override public Long deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||
if (parser.currentToken() != JsonToken.VALUE_NUMBER_INT)
|
||||
return (Long) context.handleUnexpectedToken(Long.class, parser);
|
||||
return parser.getLongValue();
|
||||
}
|
||||
}
|
||||
+2
@@ -28,6 +28,8 @@ public class OutboundPalletItem {
|
||||
private Integer pickedPackageCount;
|
||||
private String status;
|
||||
private String skuCode;
|
||||
/** Immutable piece quantity for a selected single-SKU return source. Null means legacy whole-carton work. */
|
||||
private Long returnSkuQuantity;
|
||||
private String cartonMarks;
|
||||
private String orderRefs;
|
||||
private String inboundCode;
|
||||
|
||||
+29
-8
@@ -499,6 +499,8 @@ public class DeliveryTransferService {
|
||||
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
|
||||
String warehouseId = request.overseasWarehouseId().trim();
|
||||
if (request.quantity() != null && (!StringUtils.hasText(request.skuCode()) || request.quantity() <= 0
|
||||
|| "PALLET".equals(request.unitType()))) throw conflict("按件数出库必须选择单SKU,不能同时整托出库");
|
||||
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(StringUtils::hasText).distinct().toList();
|
||||
if (marks.size() != request.cartonMarks().size()) throw conflict("箱唛不能为空且不可重复");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -514,6 +516,8 @@ public class DeliveryTransferService {
|
||||
existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
if ("PARCEL".equals(carton.getSourceUnitType()) && !StringUtils.hasText(request.skuCode()))
|
||||
throw conflict("包裹散件只能按 SKU 出库,不能按箱号或托盘出库");
|
||||
com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountGuard.requireConfirmed(carton);
|
||||
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱已被其他作业占用:" + carton.getCartonMark());
|
||||
var contents = returnCartonItems.lockCarton(carton.getId());
|
||||
@@ -521,12 +525,22 @@ public class DeliveryTransferService {
|
||||
throw conflict("退货箱缺少明细或已被合箱计划锁定:" + carton.getCartonMark());
|
||||
if (StringUtils.hasText(request.skuCode()) && (contents.size() != 1
|
||||
|| !request.skuCode().trim().equals(contents.get(0).getSkuCode())))
|
||||
throw conflict("单SKU出库只支持指定SKU的散件库存");
|
||||
if (StringUtils.hasText(request.skuCode()) && !"PARCEL".equals(carton.getSourceUnitType()))
|
||||
throw conflict("已装箱库存请按箱唛出库");
|
||||
throw conflict("单SKU出库只支持箱内仅有指定SKU的库存;混合箱请先拆箱");
|
||||
if ("PALLET".equals(request.unitType()) && !StringUtils.hasText(carton.getPalletCode()))
|
||||
throw conflict("整托出库必须存在托盘绑定");
|
||||
}
|
||||
Map<String, Long> skuQuantities = new LinkedHashMap<>();
|
||||
if (StringUtils.hasText(request.skuCode())) {
|
||||
long available = locked.stream().mapToLong(c -> value(c.getTotalQuantity())).sum();
|
||||
long remaining = request.quantity() == null ? available : request.quantity();
|
||||
if (remaining > available) throw conflict("出库件数超过所选来源库存数量");
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
long selected = Math.min(remaining, value(carton.getTotalQuantity()));
|
||||
if (selected > 0) skuQuantities.put(carton.getCartonMark(), selected);
|
||||
remaining -= selected;
|
||||
}
|
||||
locked = locked.stream().filter(c -> skuQuantities.containsKey(c.getCartonMark())).toList();
|
||||
}
|
||||
if ("PALLET".equals(request.unitType())) {
|
||||
String pallet = locked.get(0).getPalletCode();
|
||||
if (locked.stream().anyMatch(c -> !Objects.equals(pallet, c.getPalletCode()))) throw conflict("整托出库只能选择同一托盘");
|
||||
@@ -539,7 +553,7 @@ public class DeliveryTransferService {
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RETURN_OUTBOUND", warehouseId, outbound.getId()));
|
||||
outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(marks.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(locked.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
outbound.setOverseasWarehouseId(warehouseId); outbound.setIdempotencyKey(request.idempotencyKey().trim());
|
||||
outbound.setRequestHash(requestHash);
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
@@ -547,7 +561,9 @@ public class DeliveryTransferService {
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId);
|
||||
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(1); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo());
|
||||
item.setSkuCode(trim(request.skuCode()));
|
||||
// Packed stock is still picked by its physical carton mark; loose parcels use SKU scanning.
|
||||
item.setSkuCode("PARCEL".equals(carton.getSourceUnitType()) ? trim(request.skuCode()) : null);
|
||||
item.setReturnSkuQuantity(skuQuantities.get(carton.getCartonMark()));
|
||||
item.setPalletNo("PALLET".equals(request.unitType()) ? carton.getPalletCode() : null);
|
||||
item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
@@ -557,7 +573,7 @@ public class DeliveryTransferService {
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(warehouseId); delivery.setDeliveryGroupKey("RETURN:" + request.returnNo().trim());
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setDeliveryAddress(request.recipient().trim());
|
||||
delivery.setPackageCount(marks.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
delivery.setPackageCount(locked.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
delivery.setRemark(trim(request.remark())); delivery.setIdempotencyKey(childKey(request.idempotencyKey(), "RETURN-DELIVERY"));
|
||||
delivery.setVersion(0L); delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
|
||||
deliveryOrders.insert(delivery);
|
||||
@@ -822,7 +838,10 @@ public class DeliveryTransferService {
|
||||
requireReturnScan(outbound, item, request);
|
||||
if (value(item.getPickedPackageCount()) > 0) throw conflict("退货箱不可重复出库");
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
|
||||
if (item.getReturnSkuQuantity() != null)
|
||||
returnStock.shipCartonQuantity(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(),
|
||||
outbound.getOutboundNo(), pick.getId(), item.getReturnSkuQuantity());
|
||||
else returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
|
||||
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
|
||||
updateOutboundPickingStatus(customerId, outbound);
|
||||
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
|
||||
@@ -1287,7 +1306,7 @@ public class DeliveryTransferService {
|
||||
value(item.getPickedPackageCount()), item.getStatus(),
|
||||
("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|
||||
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) ? returnPickPositions(outbound, task, item)
|
||||
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
|
||||
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item), item.getReturnSkuQuantity())).toList();
|
||||
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
|
||||
}
|
||||
|
||||
@@ -1326,6 +1345,8 @@ public class DeliveryTransferService {
|
||||
|| !outbound.getCustomerId().equals(item.getCustomerId())) throw notFound("退货出库明细不存在");
|
||||
if (request.inventoryPositionId() != null || !Long.valueOf(1).equals(request.packages()) || value(item.getPackageCount()) != 1)
|
||||
throw conflict("退货出库须逐箱或逐SKU扫描,无需库存仓位");
|
||||
if (item.getReturnSkuQuantity() != null && !item.getReturnSkuQuantity().equals(request.productQuantity()))
|
||||
throw conflict("实际出库件数必须等于本明细计划件数,请核对数量");
|
||||
if (StringUtils.hasText(item.getSkuCode())) {
|
||||
if (!Objects.equals(item.getSkuCode(), trim(request.skuCode()))) throw conflict("请扫描指定的退货SKU");
|
||||
} else if (request.cartonMarks() == null || request.cartonMarks().size() != 1
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ public final class PdaRelabelResponses {
|
||||
String applicationStatus, String taskStatus, String operationType, int itemCount,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages, int version, boolean claimedByCurrentOperator,
|
||||
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt) { }
|
||||
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt,
|
||||
String customerName) { }
|
||||
|
||||
public record Detail(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
|
||||
String contactName, String contactPhone, String applicationStatus, String operationType,
|
||||
|
||||
+21
-6
@@ -72,6 +72,7 @@ public class PdaRelabelService {
|
||||
@Resource private RelabelPhotoFileGateway photoFiles;
|
||||
@Resource private ObjectMapper sourceObjectMapper;
|
||||
@Resource private com.baiyue.lastleg.mapper.LastlegOrderMapper tailOrders;
|
||||
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
@Resource private com.baiyue.lastleg.exceptionorder.service.ExceptionOrderService exceptionOrders;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@@ -84,14 +85,27 @@ public class PdaRelabelService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PdaRelabelResponses.Candidate> candidates(String operatorKey, long overseasWarehouseId) {
|
||||
return applications.selectList(new LambdaQueryWrapper<RelabelApplication>()
|
||||
List<RelabelApplication> rows = applications.selectList(new LambdaQueryWrapper<RelabelApplication>()
|
||||
.eq(RelabelApplication::getOverseasWarehouseId, String.valueOf(overseasWarehouseId))
|
||||
.in(RelabelApplication::getStatus, RelabelApplicationService.PENDING_PROCESSING, PROCESSING,
|
||||
RelabelApplicationService.CUSTOMER_CONFIRM_PENDING)
|
||||
.orderByAsc(RelabelApplication::getLabelDeadline)
|
||||
.orderByAsc(RelabelApplication::getCreatedAt))
|
||||
.stream().map(application -> candidate(application,
|
||||
tasks.find(application.getCustomerId(), application.getId()), operatorKey)).toList();
|
||||
.orderByAsc(RelabelApplication::getCreatedAt));
|
||||
List<Long> orderIds = rows.stream().map(RelabelApplication::getLastlegOrderId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, com.baiyue.lastleg.entity.LastlegOrder> linkedOrders = orderIds.isEmpty() ? Map.of()
|
||||
: tailOrders.selectBatchIds(orderIds).stream().collect(Collectors.toMap(
|
||||
com.baiyue.lastleg.entity.LastlegOrder::getId, order -> order));
|
||||
Map<String, String> companyNames = new HashMap<>();
|
||||
return rows.stream().map(application -> {
|
||||
var order = linkedOrders.get(application.getLastlegOrderId());
|
||||
String customerName = order != null && Objects.equals(application.getCustomerId(), order.getCustomerId())
|
||||
? order.getCustomerName() : null;
|
||||
if (!StringUtils.hasText(customerName)) {
|
||||
customerName = companyNames.computeIfAbsent(application.getCustomerId(), customerId -> customerDirectory.find(customerId).name());
|
||||
}
|
||||
return candidate(application, tasks.find(application.getCustomerId(), application.getId()), operatorKey, customerName);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -458,6 +472,7 @@ public class PdaRelabelService {
|
||||
"manualCompletion", request.manualCompletion()));
|
||||
applicationService.changeStatus(application.getCustomerId(), relabelNo, operatorKey,
|
||||
new RelabelRequests.ChangeStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, request.remark()));
|
||||
applicationService.settlePdaCompletion(application.getCustomerId(), relabelNo, operatorKey);
|
||||
application.setStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING);
|
||||
PdaRelabelResponses.Detail result = detail(application, task);
|
||||
saveOperation(task, operatorKey, "COMPLETE", request.idempotencyKey(), command, result);
|
||||
@@ -704,7 +719,7 @@ public class PdaRelabelService {
|
||||
}
|
||||
|
||||
private PdaRelabelResponses.Candidate candidate(RelabelApplication application, PdaRelabelTask task,
|
||||
String operatorKey) {
|
||||
String operatorKey, String customerName) {
|
||||
return new PdaRelabelResponses.Candidate(application.getRelabelNo(), application.getCustomerOrderNo(),
|
||||
application.getLabelDeadline(), application.getStatus(), effectiveTaskStatus(application, task),
|
||||
operationType(application), value(application.getItemCount()),
|
||||
@@ -713,7 +728,7 @@ public class PdaRelabelService {
|
||||
task == null ? 0 : value(task.getExceptionPackages()), task == null ? 0 : value(task.getVersion()),
|
||||
task != null && PROCESSING.equals(task.getStatus()) && operatorKey.equals(task.getOperatorKey()),
|
||||
task == null ? null : task.getReviewDecision(), task == null ? null : task.getReviewRemark(),
|
||||
task == null ? null : task.getReviewedAt());
|
||||
task == null ? null : task.getReviewedAt(), customerName);
|
||||
}
|
||||
|
||||
private PdaRelabelResponses.Detail detail(RelabelApplication application, PdaRelabelTask task) {
|
||||
|
||||
+9
-1
@@ -850,7 +850,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
private void publishCompletionFeesAndSettlement(RelabelApplication application, String operatorId,
|
||||
List<LastlegOrder> linkedOrders) {
|
||||
if ("NOT_REQUIRED".equals(application.getWalletStatus())) return;
|
||||
if (Set.of("NOT_REQUIRED", "SETTLE_PENDING", "SETTLED").contains(Objects.toString(application.getWalletStatus(), ""))) return;
|
||||
BigDecimal quantity = BigDecimal.valueOf(Objects.requireNonNullElse(application.getTotalPackageCount(), 0));
|
||||
String customerId = application.getCustomerId(), relabelNo = application.getRelabelNo();
|
||||
feeSources.publishApproved(operatorId, "RELABEL:" + relabelNo,
|
||||
@@ -868,6 +868,14 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setWalletStatus("SETTLE_PENDING"); applications.updateById(application);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void settlePdaCompletion(String customerId, String relabelNo, String operatorId) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
if (!CUSTOMER_CONFIRM_PENDING.equals(application.getStatus())) throw conflict("换标尚未完成,不能扣款");
|
||||
publishCompletionFeesAndSettlement(application, operatorId, tailOrders(application));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail confirmCompletion(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.CustomerConfirm request) {
|
||||
|
||||
+6
@@ -12,6 +12,12 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnInventoryBalanceMapper extends BaseMapper<ReturnInventoryBalance> {
|
||||
@Update("UPDATE ll_return_inventory_balance SET total_quantity=total_quantity-#{quantity}, " +
|
||||
"available_quantity=available_quantity-#{quantity},updated_at=NOW(3) " +
|
||||
"WHERE id=#{id} AND customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} " +
|
||||
"AND total_quantity>=#{quantity} AND available_quantity>=#{quantity} AND carton_quantity>=1")
|
||||
int deductOutboundPieces(@Param("id") long id, @Param("customerId") String customerId,
|
||||
@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 " +
|
||||
|
||||
+34
-9
@@ -382,6 +382,17 @@ public class ReturnInventoryService {
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void shipCarton(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId) {
|
||||
shipCartonInternal(customerId, warehouseId, returnNo, cartonMark, outboundNo, outboundPickId, null);
|
||||
}
|
||||
|
||||
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void shipCartonQuantity(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId, long quantity) {
|
||||
shipCartonInternal(customerId, warehouseId, returnNo, cartonMark, outboundNo, outboundPickId, quantity);
|
||||
}
|
||||
|
||||
private void shipCartonInternal(String customerId, String warehouseId, String returnNo, String cartonMark,
|
||||
String outboundNo, long outboundPickId, Long requestedQuantity) {
|
||||
ReturnInventoryCarton carton = cartons.lockMark(required(cartonMark, "退货箱码不能为空"));
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !warehouseId.equals(carton.getOverseasWarehouseId()) || !matchesReturnSource(carton, returnNo)
|
||||
@@ -400,22 +411,27 @@ public class ReturnInventoryService {
|
||||
}
|
||||
if (contents.isEmpty() || total != value(carton.getTotalQuantity())
|
||||
|| carton.getSkuTypes() == null || contents.size() != carton.getSkuTypes()) throw conflict("退货箱内容与实物数量不一致");
|
||||
if (requestedQuantity != null && (contents.size() != 1 || requestedQuantity <= 0 || requestedQuantity > total))
|
||||
throw conflict("单SKU出库件数或来源明细不匹配");
|
||||
boolean partial = requestedQuantity != null && requestedQuantity < total;
|
||||
boolean parcel = "PARCEL".equals(carton.getSourceUnitType());
|
||||
long cartonDelta = partial || parcel ? 0 : 1;
|
||||
contents = contents.stream().sorted(Comparator.comparing(ReturnInventoryCartonItem::getSkuCode)).toList();
|
||||
String scopedCustomer = carton.getCustomerId(), scopedWarehouse = carton.getOverseasWarehouseId();
|
||||
Map<String, com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryBalance> lockedBalances = new LinkedHashMap<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
var balance = balances.lockSku(scopedCustomer, scopedWarehouse, content.getSkuCode());
|
||||
long quantity = content.getQuantity();
|
||||
long quantity = requestedQuantity == null ? content.getQuantity() : requestedQuantity;
|
||||
if (balance == null || value(balance.getTotalQuantity()) < quantity
|
||||
|| value(balance.getAvailableQuantity()) < quantity || value(balance.getCartonQuantity()) < 1) {
|
||||
|| value(balance.getAvailableQuantity()) < quantity || (!parcel && value(balance.getCartonQuantity()) < 1)) {
|
||||
throw conflict("退货库存不足,不能完成该箱出库:" + content.getSkuCode());
|
||||
}
|
||||
lockedBalances.put(content.getSkuCode(), balance);
|
||||
}
|
||||
for (ReturnInventoryCartonItem content : contents) {
|
||||
var balance = lockedBalances.get(content.getSkuCode());
|
||||
long quantity = content.getQuantity();
|
||||
long quantity = requestedQuantity == null ? content.getQuantity() : requestedQuantity;
|
||||
var movement = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryOutboundMovement();
|
||||
movement.setId(IdWorker.getId()); movement.setOutboundPickId(outboundPickId);
|
||||
movement.setCustomerId(scopedCustomer); movement.setOverseasWarehouseId(scopedWarehouse);
|
||||
@@ -424,12 +440,19 @@ public class ReturnInventoryService {
|
||||
movement.setSkuCode(content.getSkuCode()); movement.setProductNameZh(content.getProductNameZh()); movement.setQuantity(quantity);
|
||||
movement.setTotalBefore(balance.getTotalQuantity()); movement.setTotalAfter(balance.getTotalQuantity() - quantity);
|
||||
movement.setAvailableBefore(balance.getAvailableQuantity()); movement.setAvailableAfter(balance.getAvailableQuantity() - quantity);
|
||||
movement.setCartonsBefore(balance.getCartonQuantity()); movement.setCartonsAfter(balance.getCartonQuantity() - 1);
|
||||
movement.setCartonsBefore(balance.getCartonQuantity()); movement.setCartonsAfter(balance.getCartonQuantity() - cartonDelta);
|
||||
movement.setOccurredAt(now);
|
||||
if (balances.deductOutbound(balance.getId(), scopedCustomer, scopedWarehouse, quantity) != 1) throw conflict("退货库存余额已变化,请重新核对");
|
||||
int changed = partial || parcel ? balances.deductOutboundPieces(balance.getId(), scopedCustomer, scopedWarehouse, quantity)
|
||||
: balances.deductOutbound(balance.getId(), scopedCustomer, scopedWarehouse, quantity);
|
||||
if (changed != 1) throw conflict("退货库存余额已变化,请重新核对");
|
||||
if (movements.insert(movement) != 1) throw new IllegalStateException("退货出库流水写入失败");
|
||||
if (partial) {
|
||||
content.setQuantity(content.getQuantity() - quantity);
|
||||
if (cartonItems.updateById(content) != 1) throw conflict("退货箱内数量已变化,请重新核对");
|
||||
}
|
||||
}
|
||||
carton.setStatus("OUTBOUNDED"); carton.setUpdatedAt(now);
|
||||
if (partial) carton.setTotalQuantity(total - requestedQuantity);
|
||||
carton.setStatus(partial ? "AVAILABLE" : "OUTBOUNDED"); carton.setUpdatedAt(now);
|
||||
if (cartons.updateById(carton) != 1) throw conflict("退货箱状态已变化,请重新核对");
|
||||
}
|
||||
|
||||
@@ -441,7 +464,9 @@ public class ReturnInventoryService {
|
||||
required(lines.get(0).skuCode(), "SKU 不能为空");
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
// A parcel row tracks loose SKU provenance; it is never a physical carton or printable box label.
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "SP" + hash(List.of(receipt.getId(), lines.get(0).skuCode())).substring(0, 24).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();
|
||||
long total = lines.stream().mapToLong(ReceiptLine::quantity).sum();
|
||||
@@ -465,7 +490,7 @@ public class ReturnInventoryService {
|
||||
item.setSkuCode(line.skuCode()); item.setProductNameZh(text(line.productNameZh())); item.setQuantity(line.quantity());
|
||||
cartonItems.insert(item);
|
||||
balances.adjust(IdWorker.getId(), order.getCustomerId(), order.getOverseasWarehouseId(), line.skuCode(),
|
||||
text(line.productNameZh()), line.quantity(), line.quantity(), 1);
|
||||
text(line.productNameZh()), line.quantity(), line.quantity(), "PARCEL".equals(receipt.getUnitType()) ? 0 : 1);
|
||||
}
|
||||
if ("BOX".equals(receipt.getUnitType())) {
|
||||
var contents = lines.stream().map(line -> new com.baiyue.lastleg.returnreceipt.dto.ReturnCartonCountDtos.Sku(line.skuCode(), line.productNameZh(), line.quantity())).toList();
|
||||
@@ -968,7 +993,7 @@ public class ReturnInventoryService {
|
||||
productNames.putIfAbsent(sku, item.getProductNameZh());
|
||||
item.setQuantity(item.getQuantity() - take);
|
||||
if (item.getQuantity() == 0) cartonItems.deleteById(item.getId()); else cartonItems.updateById(item);
|
||||
if (item.getQuantity() == 0) emptiedBySku.merge(sku, 1, Integer::sum);
|
||||
if (item.getQuantity() == 0 && !"PARCEL".equals(source.getSourceUnitType())) emptiedBySku.merge(sku, 1, Integer::sum);
|
||||
}
|
||||
if (item.getQuantity() > 0) { sourceTotal = Math.addExact(sourceTotal, item.getQuantity()); sourceTypes++; }
|
||||
}
|
||||
|
||||
+7
-4
@@ -255,7 +255,7 @@ public class ReturnReceiptService {
|
||||
.toList();
|
||||
long boxes = value(receipt.getTotalBoxCount());
|
||||
if (boxes == 0) boxes = lines.stream().mapToLong(ReturnOrderResponses.ReceiptItem::boxCount).sum();
|
||||
String cartonMark = text(receipt.getCartonMark());
|
||||
String cartonMark = "PARCEL".equals(receipt.getUnitType()) ? null : text(receipt.getCartonMark());
|
||||
List<ReturnInventoryService.ReturnOrderLine> labelLines = lines.stream()
|
||||
.map(line -> new ReturnInventoryService.ReturnOrderLine(line.skuCode(), line.productNameZh(), line.quantity(), line.boxCount()))
|
||||
.toList();
|
||||
@@ -579,6 +579,9 @@ public class ReturnReceiptService {
|
||||
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";
|
||||
if (unitCode == null && "PARCEL".equals(unitType)) {
|
||||
unitCode = "PK" + hash(List.of(order.getReturnNo(), IdWorker.getId())).substring(0, 16).toUpperCase(java.util.Locale.ROOT);
|
||||
}
|
||||
if (unitCode == null) throw badRequest(("PALLET".equals(unitType) ? "托盘码" : "包裹码") + "不能为空");
|
||||
if ("PALLET".equals(unitType)
|
||||
&& (request.length() == null || request.width() == null || request.height() == null || request.weight() == null
|
||||
@@ -633,7 +636,7 @@ public class ReturnReceiptService {
|
||||
} else {
|
||||
inventoryCartons = List.of(inventory.createReceiptCarton(order, receipt, unitCode, new ArrayList<>(inventoryLines.values())));
|
||||
}
|
||||
receipt.setCartonMark(inventoryCartons.get(0).getCartonMark());
|
||||
receipt.setCartonMark("PARCEL".equals(unitType) ? null : inventoryCartons.get(0).getCartonMark());
|
||||
receipts.updateById(receipt);
|
||||
syncTailCargo(order, receipt, allocations, now);
|
||||
List<ReturnOrderItem> finalItems = orderItems.listOrder(order.getId());
|
||||
@@ -777,10 +780,10 @@ public class ReturnReceiptService {
|
||||
return confirmed(order, receipt, COMPLETED.equals(order.getStatus()));
|
||||
}
|
||||
private PdaReturnReceiptResponses.Confirmed confirmed(ReturnOrder order, ReturnReceipt receipt, boolean completed) {
|
||||
String cartonMark = text(receipt.getCartonMark());
|
||||
String cartonMark = "PARCEL".equals(receipt.getUnitType()) ? null : text(receipt.getCartonMark());
|
||||
String printableHtml = null;
|
||||
List<ReturnOrderResponses.CartonLabel> labels = new java.util.ArrayList<>();
|
||||
if (receipt.getId() != null) {
|
||||
if (receipt.getId() != null && !"PARCEL".equals(receipt.getUnitType())) {
|
||||
for (ReturnInventoryCarton carton : cartons.listReceipt(receipt.getId())) {
|
||||
var label = inventory.label(order.getCustomerId(), carton.getCartonMark());
|
||||
if (label != null) labels.add(new ReturnOrderResponses.CartonLabel(carton.getCartonMark(), label.printableHtml()));
|
||||
|
||||
Reference in New Issue
Block a user