贴标申请,仓储转运
This commit is contained in:
+16
@@ -17,6 +17,9 @@ import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
import com.baiyue.lastleg.service.LastlegOrderCommandService;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.lastleg.wallet.WalletCommandCompletedConsumer;
|
||||
@@ -52,6 +55,7 @@ public class LastlegOrderController {
|
||||
@Resource private LastlegOrderCommandService commandService;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
@Resource private WalletCommandCompletedConsumer walletCommandCompletedConsumer;
|
||||
@Resource private RelabelApplicationService relabelApplicationService;
|
||||
|
||||
@PostMapping("/internal/lastleg/events")
|
||||
@Operation(summary = "Receive a first-leg loading or actual shipping-node event",
|
||||
@@ -163,6 +167,18 @@ public class LastlegOrderController {
|
||||
return ApiResponse.success(orderService.detail(platformScope(user, customerId), id));
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/relabel-review")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Review a directly-created labeling order in the tail-leg order workbench")
|
||||
public ApiResponse<RelabelResponses.Detail> reviewDirectRelabel(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id,
|
||||
@Valid @RequestBody RelabelRequests.ChangeStatus request) {
|
||||
return ApiResponse.success(relabelApplicationService.reviewDirectTailOrder(
|
||||
platformScope(user, customerId), id, operatorId(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/api/platform/v1/last-leg/orders/{id}/fees")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Add a fee to one tail-leg order", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
|
||||
+19
-14
@@ -430,33 +430,38 @@ public class DeliveryTransferService {
|
||||
new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
.eq(StringUtils.hasText(customerId), WarehouseTransferOrder::getCustomerId, trim(customerId))
|
||||
.eq(StringUtils.hasText(warehouseId), WarehouseTransferOrder::getOperatingWarehouseId, trim(warehouseId))
|
||||
.eq(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION")
|
||||
// 已确定目的仓的转运单在完成入库/换标后进入 READY_FOR_DISPATCH;
|
||||
// 未确定目的仓的历史单据仍保留 WAIT_FBA_DESTINATION,均应进入出库候选。
|
||||
.in(WarehouseTransferOrder::getStatus, "WAIT_FBA_DESTINATION", "READY_FOR_DISPATCH")
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
|
||||
List<DeliveryResponses.WarehouseTransferOutboundCandidate> result = new ArrayList<>();
|
||||
for (WarehouseTransferOrder transfer : transfers) {
|
||||
boolean destinationBound = "FBA".equals(transfer.getDestinationType())
|
||||
? StringUtils.hasText(transfer.getDestinationWarehouseCode())
|
||||
: "PRIVATE".equals(transfer.getDestinationType())
|
||||
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
|
||||
// This screen is the destination-completion queue. Once a real
|
||||
// destination is bound, the remaining inventory belongs in smart
|
||||
// dispatch and must disappear from this list.
|
||||
if (destinationBound) continue;
|
||||
LastlegOrder order = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
List<WarehouseTransferPallet> completedPallets = warehouseTransferPallets.listOrder(
|
||||
transfer.getCustomerId(), transfer.getId()).stream()
|
||||
.filter(pallet -> "COMPLETED".equals(pallet.getStatus()) && pallet.getWarehousePalletId() != null)
|
||||
.toList();
|
||||
long available = 0;
|
||||
for (WarehouseTransferPallet pallet : completedPallets) {
|
||||
long palletAvailable = inventoryPositions.listPallet(transfer.getCustomerId(), pallet.getWarehousePalletId())
|
||||
.stream().filter(position -> transfer.getOperatingWarehouseId().equals(position.getOverseasWarehouseId()))
|
||||
.mapToLong(position -> value(position.getAvailablePackages())).sum();
|
||||
available += palletAvailable;
|
||||
if (palletAvailable <= 0) continue;
|
||||
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
|
||||
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
|
||||
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
|
||||
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
|
||||
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
|
||||
pallet.getId(), pallet.getWarehousePalletId(), pallet.getPalletNo(), Math.toIntExact(palletAvailable),
|
||||
pallet.getGrossWeightKg(), pallet.getVolumeCbm()));
|
||||
}
|
||||
if (available <= 0 || completedPallets.isEmpty()) continue;
|
||||
result.add(new DeliveryResponses.WarehouseTransferOutboundCandidate(
|
||||
transfer.getCustomerId(), order == null ? null : order.getCustomerName(), transfer.getTransferNo(),
|
||||
transfer.getOperatingWarehouseId(), transfer.getStatus(), Boolean.TRUE.equals(transfer.getNeedRelabel()),
|
||||
transfer.getRelabelNo(), transfer.getDestinationType(), transfer.getDestinationWarehouseCode(),
|
||||
transfer.getPrivateDestinationAddress(), transfer.getPrivateDestinationPostalCode(),
|
||||
null, null, null, Math.toIntExact(available),
|
||||
completedPallets.stream().map(WarehouseTransferPallet::getGrossWeightKg).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add),
|
||||
completedPallets.stream().map(WarehouseTransferPallet::getVolumeCbm).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+32
-16
@@ -60,7 +60,7 @@ public interface DispatchCandidateMapper {
|
||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(MIN(l.fba_warehouse_code),''),NULLIF(p.destination_warehouse_code,'')) AS fbaWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||
@@ -79,15 +79,20 @@ public interface DispatchCandidateMapper {
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
|
||||
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
|
||||
"COALESCE(NULLIF(MIN(l.destination_type),''),NULLIF(p.destination_type,'')) AS destinationType,COALESCE(MIN(l.delivery_address),p.delivery_address) AS deliveryAddress," +
|
||||
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_order wt ON wt.customer_id=ip.customer_id AND wt.lastleg_order_id=ip.source_order_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||
"WHERE ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"WHERE ((ip.creation_mode='WAREHOUSE_TRANSFER' AND ((wt.destination_type='FBA' AND NULLIF(TRIM(wt.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(wt.destination_type='PRIVATE' AND NULLIF(TRIM(wt.private_destination_address),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='FBA' AND NULLIF(TRIM(p.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='PRIVATE' AND NULLIF(TRIM(p.delivery_address),'') IS NOT NULL))) OR " +
|
||||
"(ip.creation_mode!='WAREHOUSE_TRANSFER' AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)))) " +
|
||||
"<if test='customerId != null and customerId != \"\"'> AND ip.customer_id=#{customerId}</if> " +
|
||||
"<if test='planIds != null and planIds.size() > 0'> AND p.plan_id IN " +
|
||||
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></if> " +
|
||||
@@ -107,12 +112,18 @@ public interface DispatchCandidateMapper {
|
||||
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
|
||||
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id))) " +
|
||||
"AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
// Warehouse-transfer inventory is intentionally reserved before dispatch. It must still be
|
||||
// eligible for the normal smart-dispatch flow once its destination is bound; other order
|
||||
// types keep the reservation guard to prevent duplicate dispatching.
|
||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
|
||||
// A warehouse-transfer pallet may contain both relabel and non-relabel inventory.
|
||||
// Do not hide the whole pallet because one carton/SKU has an active relabel item;
|
||||
// the remaining available inventory must still be dispatchable.
|
||||
"AND (ip.creation_mode!='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE rli.customer_id=ip.customer_id AND rli.source_inbound_order_id IS NOT NULL " +
|
||||
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
|
||||
"AND rli.source_inbound_order_id=o.id AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED'))) " +
|
||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||
"ORDER BY p.plan_id,p.sequence_no" +
|
||||
@@ -160,7 +171,7 @@ public interface DispatchCandidateMapper {
|
||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(MIN(l.fba_warehouse_code),''),NULLIF(p.destination_warehouse_code,'')) AS fbaWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||
@@ -179,31 +190,36 @@ public interface DispatchCandidateMapper {
|
||||
"WHERE ri.customer_id=ip.customer_id AND rp.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND ri.fba_carton_mark IS NOT NULL AND ri.fba_carton_mark != '' " +
|
||||
"AND ra.status NOT IN ('REJECTED','CANCELLED')) AS relabelCartonMarks," +
|
||||
"MIN(l.destination_type) AS destinationType,MIN(l.delivery_address) AS deliveryAddress," +
|
||||
"COALESCE(NULLIF(MIN(l.destination_type),''),NULLIF(p.destination_type,'')) AS destinationType,COALESCE(MIN(l.delivery_address),p.delivery_address) AS deliveryAddress," +
|
||||
"MIN(l.postal_code) AS postalCode,MIN(l.contact_name) AS contactName,MIN(l.contact_phone) AS contactPhone " +
|
||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||
"LEFT JOIN ll_warehouse_transfer_order wt ON wt.customer_id=ip.customer_id AND wt.lastleg_order_id=ip.source_order_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
|
||||
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"AND ((ip.creation_mode='WAREHOUSE_TRANSFER' AND ((wt.destination_type='FBA' AND NULLIF(TRIM(wt.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(wt.destination_type='PRIVATE' AND NULLIF(TRIM(wt.private_destination_address),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='FBA' AND NULLIF(TRIM(p.destination_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(p.destination_type='PRIVATE' AND NULLIF(TRIM(p.delivery_address),'') IS NOT NULL))) OR " +
|
||||
"(ip.creation_mode!='WAREHOUSE_TRANSFER' AND ((l.destination_type='FBA' AND NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL) OR " +
|
||||
"(l.destination_type='PRIVATE' AND NULLIF(TRIM(l.delivery_address),'') IS NOT NULL AND NULLIF(TRIM(l.postal_code),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)) " +
|
||||
"AND NULLIF(TRIM(l.contact_name),'') IS NOT NULL AND NULLIF(TRIM(l.contact_phone),'') IS NOT NULL)))) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation ha " +
|
||||
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
||||
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
||||
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
||||
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id)) " +
|
||||
"AND (ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_relabel_item rli JOIN ll_relabel_application rla ON rla.id=rli.application_id AND rla.customer_id=rli.customer_id " +
|
||||
"JOIN ll_inbound_plan_line rpl ON rpl.order_id=rli.source_inbound_order_id " +
|
||||
"WHERE rli.customer_id=#{customerId} AND rli.source_inbound_order_id IS NOT NULL AND rpl.id IN " +
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id) " +
|
||||
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED')) " +
|
||||
"AND rla.status NOT IN ('REJECTED','CANCELLED','COMPLETED'))) " +
|
||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||
"ORDER BY p.id FOR UPDATE" +
|
||||
|
||||
+6
-1
@@ -241,7 +241,12 @@ public class DispatchPdaWorkflowService {
|
||||
}
|
||||
}
|
||||
for (DispatchItem item : dispatchItems) {
|
||||
if (item.getOrderId() != null) warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
|
||||
// A relabel dispatch may consume only part of a warehouse-transfer
|
||||
// pallet. Do not close the whole transfer by its parent order; the
|
||||
// remaining cartons still need destination binding/outbound.
|
||||
if (item.getOrderId() != null && !"RELABEL_ORDER".equals(item.getSourceBusinessType())) {
|
||||
warehouseTransfers.markOutboundByOrder(customerId, item.getOrderId(), now);
|
||||
}
|
||||
if ("RELABEL_ORDER".equals(item.getSourceBusinessType()) && item.getSourceBusinessNo() != null) {
|
||||
warehouseTransfers.markOutboundByRelabel(customerId, item.getSourceBusinessNo(), now);
|
||||
}
|
||||
|
||||
+6
@@ -53,6 +53,12 @@ public class MerchantRelabelApplicationController {
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo) { return ApiResponse.success(service.detail(customer(user), relabelNo)); }
|
||||
@PutMapping("/{relabelNo}/resubmit")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_CREATE + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> resubmit(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @Valid @RequestBody RelabelRequests.Resubmit request) {
|
||||
return ApiResponse.success(service.resubmit(customer(user), relabelNo, operator(user), request));
|
||||
}
|
||||
@PostMapping(value = "/{relabelNo}/shipping-label", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_FILES + "')")
|
||||
public ApiResponse<RelabelResponses.Detail> uploadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -129,6 +129,12 @@ public final class RelabelRequests {
|
||||
public record ChangeStatus(@NotBlank @Size(max = 32) String status,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record Resubmit(@NotNull LocalDateTime labelDeadline,
|
||||
@NotBlank @Size(max = 128) String contactName,
|
||||
@NotBlank @Size(max = 64) String contactPhone,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
public record CustomerConfirm(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public final class RelabelResponses {
|
||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
String fulfillmentStatus, String outboundOrderNo, LocalDateTime customerConfirmedAt,
|
||||
LocalDateTime createdAt) {}
|
||||
String reviewChannel, String reviewRemark, LocalDateTime createdAt) {}
|
||||
public record Item(Long id, String skuCode, String productImageFileId, String productImageFileName,
|
||||
String productImageUrl, String productNameZh, int totalPackageCount,
|
||||
int notifiedPackageCount, String shipmentNo, String trackingCode,
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ public final class PdaRelabelResponses {
|
||||
public record Candidate(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
|
||||
String applicationStatus, String taskStatus, String operationType, int itemCount,
|
||||
int expectedPackages, int relabelledPackages, int skippedPackages,
|
||||
int exceptionPackages, int version, boolean claimedByCurrentOperator) { }
|
||||
int exceptionPackages, int version, boolean claimedByCurrentOperator,
|
||||
String reviewDecision, String reviewRemark, LocalDateTime reviewedAt) { }
|
||||
|
||||
public record Detail(String relabelNo, String customerOrderNo, LocalDateTime labelDeadline,
|
||||
String contactName, String contactPhone, String applicationStatus, String operationType,
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ public interface PdaRelabelTaskMapper extends BaseMapper<PdaRelabelTask> {
|
||||
relabelled_packages=#{task.relabelledPackages}, skipped_packages=#{task.skippedPackages},
|
||||
exception_packages=#{task.exceptionPackages}, completion_photo_file_ids=#{task.completionPhotoFileIds},
|
||||
completion_remark=#{task.completionRemark}, review_decision=#{task.reviewDecision},
|
||||
review_remark=#{task.reviewRemark}, submitted_at=#{task.submittedAt}, reviewed_at=#{task.reviewedAt},
|
||||
review_remark=#{task.reviewRemark}, claimed_at=#{task.claimedAt},
|
||||
submitted_at=#{task.submittedAt}, reviewed_at=#{task.reviewedAt},
|
||||
version=version+1, updated_at=#{task.updatedAt}
|
||||
WHERE id=#{task.id} AND customer_id=#{task.customerId} AND version=#{expectedVersion}
|
||||
""")
|
||||
|
||||
+16
-4
@@ -53,6 +53,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class PdaRelabelService {
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard bindingGuard;
|
||||
public static final String AVAILABLE = "AVAILABLE";
|
||||
public static final String PROCESSING = "PROCESSING";
|
||||
public static final String WAIT_REVIEW = "WAIT_REVIEW";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
@@ -116,7 +117,16 @@ public class PdaRelabelService {
|
||||
throw conflict("当前账号正在执行换标单 " + activeTask.getRelabelNo() + ",请先完成后再操作其他换标单");
|
||||
}
|
||||
if (task != null) {
|
||||
if (!operatorKey.equals(task.getOperatorKey()) || !PROCESSING.equals(task.getStatus())) {
|
||||
if (AVAILABLE.equals(task.getStatus()) && "REWORK".equals(task.getReviewDecision())) {
|
||||
int expectedVersion = task.getVersion();
|
||||
task.setStatus(PROCESSING);
|
||||
task.setOperatorKey(operatorKey);
|
||||
task.setOperatorName(operatorName);
|
||||
task.setClaimedAt(LocalDateTime.now());
|
||||
updateTask(task, expectedVersion);
|
||||
insertHistory(task, "REWORK_CLAIM", AVAILABLE, PROCESSING, operatorKey, operatorName,
|
||||
Map.of("relabelNo", relabelNo));
|
||||
} else if (!operatorKey.equals(task.getOperatorKey()) || !PROCESSING.equals(task.getStatus())) {
|
||||
throw conflict("Relabel task is already claimed or awaiting review");
|
||||
}
|
||||
} else {
|
||||
@@ -488,7 +498,7 @@ public class PdaRelabelService {
|
||||
requireVersion(task, request.expectedVersion());
|
||||
// Preserve the previous round before freeing the active-label uniqueness constraint.
|
||||
List<PdaRelabelUnitFact> previousFacts = factList(task);
|
||||
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, PROCESSING, operatorKey, operatorName,
|
||||
insertHistory(task, "REWORK_ARCHIVE", WAIT_REVIEW, AVAILABLE, operatorKey, operatorName,
|
||||
Map.of("facts", previousFacts, "photoFileIds", Objects.toString(task.getCompletionPhotoFileIds(), "[]"),
|
||||
"completionRemark", Objects.toString(task.getCompletionRemark(), ""), "remark", request.remark()));
|
||||
for (PdaRelabelUnitFact fact : previousFacts) {
|
||||
@@ -501,7 +511,7 @@ public class PdaRelabelService {
|
||||
applicationService.resetForRework(application.getCustomerId(), relabelNo, operatorKey, request.remark());
|
||||
application.setStatus(PROCESSING);
|
||||
String before = task.getStatus();
|
||||
String after = PROCESSING;
|
||||
String after = AVAILABLE;
|
||||
task.setStatus(after); task.setReviewerKey(operatorKey); task.setReviewerName(operatorName);
|
||||
task.setReviewDecision(decision); task.setReviewRemark(trim(request.remark()));
|
||||
task.setReviewedAt(LocalDateTime.now());
|
||||
@@ -694,7 +704,9 @@ public class PdaRelabelService {
|
||||
task == null ? value(application.getNotifiedPackageCount()) : value(task.getExpectedPackages()),
|
||||
task == null ? 0 : value(task.getRelabelledPackages()), task == null ? 0 : value(task.getSkippedPackages()),
|
||||
task == null ? 0 : value(task.getExceptionPackages()), task == null ? 0 : value(task.getVersion()),
|
||||
task != null && operatorKey.equals(task.getOperatorKey()));
|
||||
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());
|
||||
}
|
||||
|
||||
private PdaRelabelResponses.Detail detail(RelabelApplication application, PdaRelabelTask task) {
|
||||
|
||||
+106
-19
@@ -332,7 +332,11 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
private RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request,
|
||||
Long existingTailOrderId) {
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
boolean requiresOutbound = request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
LastlegOrder linkedSourceOrder = existingTailOrderId == null ? null : orders.selectById(existingTailOrderId);
|
||||
boolean warehouseTransferRelabel = linkedSourceOrder != null
|
||||
&& "WAREHOUSE_TRANSFER".equals(linkedSourceOrder.getOrderType());
|
||||
boolean requiresOutbound = warehouseTransferRelabel
|
||||
|| request.items().stream().anyMatch(item -> !"SKU_LABEL".equals(operationType(item)));
|
||||
String deliveryMethod = trim(request.deliveryMethod());
|
||||
if (requiresOutbound && (!StringUtils.hasText(deliveryMethod) || !DELIVERY_METHODS.contains(deliveryMethod))) {
|
||||
throw badRequest("Delivery method must be TRUCK, PICKUP or EXPRESS when carton labels are changed");
|
||||
@@ -446,7 +450,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
application.setPrintShippingLabel(Boolean.TRUE.equals(request.printShippingLabel()));
|
||||
application.setFulfillmentStatus(requiresOutbound ? "WAIT_RELABEL" : "NOT_REQUIRED");
|
||||
application.setRemark(trim(request.remark()));
|
||||
application.setStatus(PENDING_PROCESSING);
|
||||
application.setStatus(PENDING_REVIEW);
|
||||
application.setItemCount(request.items().size());
|
||||
application.setTotalPackageCount(total);
|
||||
application.setNotifiedPackageCount(notified);
|
||||
@@ -476,7 +480,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
for (SourceInventory source : sources) {
|
||||
insertItem(customerId, applicationId, source, now);
|
||||
}
|
||||
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
|
||||
insertHistory(customerId, applicationId, null, PENDING_REVIEW, operatorId, "Relabel application submitted for review", now);
|
||||
if (existingTailOrderId == null) {
|
||||
tailOrders.forEach(order -> insertOrderHistory(customerId, order.getId(), null,
|
||||
order.getCurrentStatus(), "Relabel application submitted", now));
|
||||
@@ -541,6 +545,24 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
@Transactional
|
||||
public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.ChangeStatus request) {
|
||||
return changeStatus(customerId, relabelNo, operatorId, request, false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail reviewDirectTailOrder(String customerId, long orderId, String operatorId,
|
||||
RelabelRequests.ChangeStatus request) {
|
||||
LastlegOrder order = orders.selectById(orderId);
|
||||
if (order == null || !customerId.equals(order.getCustomerId())
|
||||
|| !"RELABEL_APPLICATION".equals(order.getOrderType())
|
||||
|| !StringUtils.hasText(order.getRelabelOrderNo())) {
|
||||
throw notFound("Direct relabel tail order not found");
|
||||
}
|
||||
return changeStatus(customerId, order.getRelabelOrderNo(), operatorId, request, true);
|
||||
}
|
||||
|
||||
private RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.ChangeStatus request,
|
||||
boolean directTailOrderReview) {
|
||||
RelabelApplication application = StringUtils.hasText(customerId)
|
||||
? applications.lockNo(customerId, relabelNo)
|
||||
: applications.lockAnyByRelabelNo(relabelNo);
|
||||
@@ -548,6 +570,16 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
customerId = application.getCustomerId();
|
||||
String next = request.status().trim().toUpperCase();
|
||||
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
|
||||
if ("REJECTED".equals(next) && !StringUtils.hasText(request.remark())) {
|
||||
throw badRequest("审核驳回必须填写原因");
|
||||
}
|
||||
LastlegOrder primaryOrder = orders.selectById(application.getLastlegOrderId());
|
||||
boolean directApplication = primaryOrder != null && "RELABEL_APPLICATION".equals(primaryOrder.getOrderType());
|
||||
if (PENDING_REVIEW.equals(application.getStatus()) && directApplication != directTailOrderReview) {
|
||||
throw conflict(directApplication
|
||||
? "直接创建的贴标单请在尾程订单审核"
|
||||
: "关联业务产生的换标单请在换标单审核");
|
||||
}
|
||||
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
|
||||
validateTransition(application.getStatus(), next);
|
||||
if (PENDING_REVIEW.equals(application.getStatus()) && PENDING_PROCESSING.equals(next)
|
||||
@@ -614,7 +646,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
case CUSTOMER_CONFIRM_PENDING -> mainStatus;
|
||||
case "COMPLETED" -> "RETURN_RELABEL".equals(order.getOrderType())
|
||||
? "PENDING_ASSIGNMENT" : "COMPLETED";
|
||||
case "REJECTED", "CANCELLED" -> "CANCELLED";
|
||||
case "REJECTED" -> directApplication ? "REJECTED" : "CANCELLED";
|
||||
case "CANCELLED" -> "CANCELLED";
|
||||
default -> mainStatus;
|
||||
};
|
||||
// Reviewing/processing a return relabel must not move the already
|
||||
@@ -700,6 +733,51 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail resubmit(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.Resubmit request) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
if (!"REJECTED".equals(application.getStatus())) {
|
||||
throw conflict("Only a rejected relabel application can be resubmitted");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int holdIndex = 0;
|
||||
for (RelabelItem item : itemList(application.getId())) {
|
||||
if (item.getReturnCartonId() != null) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId());
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId())
|
||||
|| !"AVAILABLE".equals(carton.getStatus())
|
||||
|| returnCartons.markRelabelPending(carton.getId(), application.getId(), relabelNo, now) != 1) {
|
||||
throw conflict("退货箱已被其他作业占用,无法重新提交");
|
||||
}
|
||||
} else if (item.getSourceInventoryPositionId() != null
|
||||
&& Objects.requireNonNullElse(item.getNotifiedPackageCount(), 0) > 0) {
|
||||
inventoryCommands.hold(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
item.getSourceInventoryPositionId(), item.getNotifiedPackageCount(), relabelNo, null,
|
||||
"RELABEL:" + relabelNo, request.idempotencyKey().trim() + ":RESUBMIT-HOLD:" + holdIndex++,
|
||||
"换标驳回修改后重新冻结库存"));
|
||||
}
|
||||
}
|
||||
application.setLabelDeadline(request.labelDeadline());
|
||||
application.setContactName(request.contactName().trim());
|
||||
application.setContactPhone(request.contactPhone().trim());
|
||||
application.setRemark(trim(request.remark()));
|
||||
application.setStatus(PENDING_REVIEW);
|
||||
application.setFulfillmentStatus("WAIT_REVIEW");
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
for (LastlegOrder order : tailOrders(application)) {
|
||||
order.setRelabelStatus(PENDING_REVIEW);
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW);
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
}
|
||||
insertHistory(customerId, application.getId(), "REJECTED", PENDING_REVIEW, operatorId,
|
||||
request.remark(), now);
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void resetForRework(String customerId, String relabelNo, String operatorId, String remark) {
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
@@ -801,19 +879,22 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (linkedTransfer != null) {
|
||||
WarehouseTransferOrder transfer = linkedTransfer;
|
||||
if (transfer != null && "WAIT_RELABEL".equals(transfer.getStatus())) {
|
||||
Set<String> destinations = relabelItems.stream()
|
||||
.map(RelabelItem::getDestinationFbaWarehouseCode)
|
||||
.filter(StringUtils::hasText).map(String::trim).collect(Collectors.toSet());
|
||||
if (!StringUtils.hasText(transfer.getFbaWarehouseCode()) && destinations.size() == 1) {
|
||||
transfer.setFbaWarehouseCode(destinations.iterator().next());
|
||||
}
|
||||
String transferStatus = StringUtils.hasText(transfer.getFbaWarehouseCode())
|
||||
// Item-level relabel destinations apply only to those cartons.
|
||||
// They must never bind the whole transfer, because its remaining
|
||||
// cartons may still require a separate destination selection.
|
||||
boolean transferHasDestination = "FBA".equals(transfer.getDestinationType())
|
||||
? StringUtils.hasText(transfer.getDestinationWarehouseCode())
|
||||
: "PRIVATE".equals(transfer.getDestinationType())
|
||||
&& StringUtils.hasText(transfer.getPrivateDestinationAddress());
|
||||
String transferStatus = transferHasDestination
|
||||
? "READY_FOR_DISPATCH" : "WAIT_FBA_DESTINATION";
|
||||
transfer.setStatus(transferStatus);
|
||||
transfer.setRelabelCompletedAt(now);
|
||||
transfer.setUpdatedAt(now);
|
||||
warehouseTransfers.updateById(transfer);
|
||||
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus)
|
||||
boolean relabelHasDestination = !relabelItems.isEmpty() && relabelItems.stream()
|
||||
.allMatch(item -> StringUtils.hasText(item.getDestinationFbaWarehouseCode()));
|
||||
application.setFulfillmentStatus("READY_FOR_DISPATCH".equals(transferStatus) || relabelHasDestination
|
||||
? "WAIT_PRE_DISPATCH" : transferStatus);
|
||||
applications.updateById(application);
|
||||
// 转运单自身的尾程订单与换标申请尾程订单是两条记录,需同步释放原转运单参与排车。
|
||||
@@ -945,6 +1026,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
|
||||
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
||||
value.getFulfillmentStatus(), value.getOutboundOrderNo(), value.getCustomerConfirmedAt(),
|
||||
order != null && "RELABEL_APPLICATION".equals(order.getOrderType()) ? "TAIL_ORDER" : "RELABEL_ORDER",
|
||||
null,
|
||||
value.getCreatedAt());
|
||||
}
|
||||
|
||||
@@ -996,8 +1079,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
.and(w -> w.eq(LastlegOrder::getSourceOrderNo, customerOrderNo).or().eq(LastlegOrder::getOrderNo, customerOrderNo))
|
||||
.last("LIMIT 1"));
|
||||
if (existingReturn != null) {
|
||||
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_PROCESSING);
|
||||
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_PROCESSING); existingReturn.setPackageCount(packageCount);
|
||||
existingReturn.setRelabelOrderNo(relabelNo); existingReturn.setRelabelStatus(PENDING_REVIEW);
|
||||
existingReturn.setSubOrderStatus("RETURN_RELABEL:" + PENDING_REVIEW); existingReturn.setPackageCount(packageCount);
|
||||
existingReturn.setUpdatedAt(now); orders.updateById(existingReturn); return existingReturn;
|
||||
}
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
@@ -1008,9 +1091,9 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
order.setSourceInboundId(relabelNo + ":" + warehouseId);
|
||||
order.setSourceOrderNo(request.customerOrderNo().trim()); order.setOverseasWarehouseId(warehouseId);
|
||||
order.setPalletCount(0); order.setPackageCount(packageCount); order.setPickedPackageCount(0);
|
||||
order.setShippedPackageCount(0); order.setCurrentStatus("WAIT_RELABEL");
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_PROCESSING); order.setRelabelOrderNo(relabelNo);
|
||||
order.setRelabelStatus(PENDING_PROCESSING); order.setCustomerRemark(trim(request.remark()));
|
||||
order.setShippedPackageCount(0); order.setCurrentStatus(PENDING_REVIEW);
|
||||
order.setSubOrderStatus("RELABEL:" + PENDING_REVIEW); order.setRelabelOrderNo(relabelNo);
|
||||
order.setRelabelStatus(PENDING_REVIEW); order.setCustomerRemark(trim(request.remark()));
|
||||
order.setSourceEventId("CUSTOMER_RELABEL:" + relabelNo + ":" + warehouseId);
|
||||
order.setSourceLoadingPlanId("RELABEL_APPLICATION"); order.setSourcePayloadHash("sha256:" + requestHash);
|
||||
order.setCreatedAt(now); order.setUpdatedAt(now); orders.insert(order);
|
||||
@@ -1023,8 +1106,12 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) {
|
||||
throw conflict("Existing tail order does not belong to this customer");
|
||||
}
|
||||
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_PROCESSING);
|
||||
order.setPackageCount(packageCount); order.setUpdatedAt(now); orders.updateById(order);
|
||||
order.setRelabelOrderNo(relabelNo); order.setRelabelStatus(PENDING_REVIEW);
|
||||
// A linked warehouse-transfer relabel can cover only part of the
|
||||
// transfer. Keep the authoritative whole-order package count so later
|
||||
// dispatches can attribute the remaining cartons correctly.
|
||||
if (!"WAREHOUSE_TRANSFER".equals(order.getOrderType())) order.setPackageCount(packageCount);
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ public final class WarehouseTransferResponses {
|
||||
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
|
||||
String destinationType, String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo,
|
||||
String relabelApplicationStatus, String relabelFulfillmentStatus,
|
||||
String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {}
|
||||
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
|
||||
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
||||
|
||||
+5
-1
@@ -23,7 +23,11 @@ import org.apache.ibatis.annotations.Update;
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND lastleg_order_id=#{orderId} AND outbound_at IS NULL")
|
||||
int markOutboundByOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
@Update("UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=#{outboundAt},updated_at=#{outboundAt} WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND outbound_at IS NULL")
|
||||
@Update("UPDATE ll_warehouse_transfer_order wto SET wto.status='OUTBOUNDED',wto.outbound_at=#{outboundAt},wto.updated_at=#{outboundAt} " +
|
||||
"WHERE wto.customer_id=#{customerId} AND wto.relabel_no=#{relabelNo} AND wto.outbound_at IS NULL " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_pallet wtp " +
|
||||
"JOIN ll_inventory_position ip ON ip.customer_id=wtp.customer_id AND ip.pallet_id=wtp.warehouse_pallet_id " +
|
||||
"WHERE wtp.customer_id=wto.customer_id AND wtp.transfer_order_id=wto.id AND ip.available_packages>0)")
|
||||
int markOutboundByRelabel(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo,
|
||||
@Param("outboundAt") java.time.LocalDateTime outboundAt);
|
||||
}
|
||||
|
||||
+33
-19
@@ -147,8 +147,10 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
validateTransferDestinationRelabel(request.destinationType(), request.fbaWarehouseCode(), request.destinationWarehouseCode(),
|
||||
request.needRelabel());
|
||||
// A split relabel transfer may intentionally defer the whole-order
|
||||
// destination: relabeled cartons go through relabel dispatch while the
|
||||
// remaining cartons are bound later from warehouse-transfer outbound.
|
||||
// All other transfer types must still select FBA or PRIVATE up front.
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
WarehouseTransferOrder replay = transfers.findKey(customerId, key);
|
||||
@@ -279,8 +281,6 @@ public class WarehouseTransferService {
|
||||
validateDestination(request.destinationType(), request.destinationWarehouseCode(), request.fbaWarehouseCode(),
|
||||
request.privateDestinationAddress(), request.privateDestinationPostalCode(),
|
||||
request.privateDestinationContactName(), request.privateDestinationContactPhone());
|
||||
validateTransferDestinationRelabel(request.destinationType(), request.fbaWarehouseCode(), request.destinationWarehouseCode(),
|
||||
request.needRelabel());
|
||||
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -422,12 +422,16 @@ public class WarehouseTransferService {
|
||||
.le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo())
|
||||
.orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId);
|
||||
Page<WarehouseTransferOrder> result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(result.getRecords());
|
||||
List<WarehouseTransferResponses.Summary> records = result.getRecords().stream().map(value ->
|
||||
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
|
||||
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
|
||||
value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(),
|
||||
value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
|
||||
value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||
return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
@@ -911,7 +915,7 @@ public class WarehouseTransferService {
|
||||
transfers.updateById(transfer);
|
||||
LastlegOrder order = requireTailOrder(customerId, transfer); String before = order.getCurrentStatus();
|
||||
order.setCurrentStatus(INBOUND_COMPLETED);
|
||||
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_PROCESSING" : "NOT_REQUIRED");
|
||||
order.setRelabelStatus(Boolean.TRUE.equals(transfer.getNeedRelabel()) ? "PENDING_REVIEW" : "NOT_REQUIRED");
|
||||
order.setSubOrderStatus("WAREHOUSE_TRANSFER:" + transfer.getStatus() + ";RELABEL:" + order.getRelabelStatus());
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
if (!java.util.Objects.equals(before, order.getCurrentStatus())) {
|
||||
@@ -1214,18 +1218,34 @@ public class WarehouseTransferService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<WarehouseTransferResponses.Summary> pdaCandidates(long overseasWarehouseId) {
|
||||
return transfers.selectList(new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
List<WarehouseTransferOrder> candidates = transfers.selectList(new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||
.eq(WarehouseTransferOrder::getOperatingWarehouseId, String.valueOf(overseasWarehouseId))
|
||||
.in(WarehouseTransferOrder::getStatus, List.of(WAIT_PDA_RECEIVING, APPROVED, ALLOCATED, WAIT_RELABEL, READY_FOR_DISPATCH))
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt)).stream().map(value ->
|
||||
.orderByDesc(WarehouseTransferOrder::getUpdatedAt));
|
||||
Map<String, RelabelApplication> relabelApplicationsByNo = relabelApplications(candidates);
|
||||
return candidates.stream().map(value ->
|
||||
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
|
||||
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
|
||||
value.getFbaWarehouseCode(), value.getDestinationType(), value.getDestinationWarehouseCode(),
|
||||
value.getDestinationWarehouseName(), value.getPackageCount(), value.getDeliveryMethod(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
|
||||
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getStatus(),
|
||||
relabelApplicationsByNo.get(value.getRelabelNo()) == null ? null : relabelApplicationsByNo.get(value.getRelabelNo()).getFulfillmentStatus(),
|
||||
value.getStatus(),
|
||||
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||
}
|
||||
|
||||
private Map<String, RelabelApplication> relabelApplications(List<WarehouseTransferOrder> transferOrders) {
|
||||
Set<String> relabelNos = transferOrders.stream().map(WarehouseTransferOrder::getRelabelNo)
|
||||
.filter(StringUtils::hasText).collect(Collectors.toSet());
|
||||
if (relabelNos.isEmpty()) return Map.of();
|
||||
List<RelabelApplication> applications = relabelApplications.selectList(
|
||||
new LambdaQueryWrapper<RelabelApplication>().in(RelabelApplication::getRelabelNo, relabelNos));
|
||||
if (applications == null || applications.isEmpty()) return Map.of();
|
||||
return applications.stream().collect(Collectors.toMap(RelabelApplication::getRelabelNo,
|
||||
Function.identity(), (left, right) -> right));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public WarehouseTransferResponses.Detail pdaDetail(long overseasWarehouseId, String transferNo) {
|
||||
WarehouseTransferOrder transfer = transfers.findNoByWarehouse(transferNo, String.valueOf(overseasWarehouseId));
|
||||
@@ -1391,19 +1411,13 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateTransferDestinationRelabel(String destinationType, String fbaWarehouseCode,
|
||||
String destinationWarehouseCode, boolean needRelabel) {
|
||||
private static void validateTransferDestinationRequired(String destinationType, String fbaWarehouseCode,
|
||||
boolean allowDeferredForRelabel) {
|
||||
String type = trim(destinationType);
|
||||
if (type == null && StringUtils.hasText(fbaWarehouseCode)) type = "FBA";
|
||||
if (type == null || "NONE".equalsIgnoreCase(type)) {
|
||||
if ((type == null || "NONE".equalsIgnoreCase(type)) && !allowDeferredForRelabel) {
|
||||
throw badRequest("仓储转运必须选择 FBA 仓或私人仓");
|
||||
}
|
||||
if ("FBA".equalsIgnoreCase(type) && !needRelabel) {
|
||||
throw badRequest("转运到 FBA 仓必须同时创建贴标单");
|
||||
}
|
||||
if ("PRIVATE".equalsIgnoreCase(type) && needRelabel) {
|
||||
throw badRequest("不需要贴标时请选择私人仓作为目的地");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyDestination(WarehouseTransferOrder transfer, WarehouseTransferRequests.Create request) {
|
||||
@@ -1598,7 +1612,7 @@ public class WarehouseTransferService {
|
||||
transfer.getOperatingWarehouseId(), transfer.getLabelDeadline(), transfer.getRelabelContactName(),
|
||||
transfer.getRelabelContactPhone(), transfer.getRelabelChargeAmount(), "仓储转运确认后自动创建换标单",
|
||||
"WAREHOUSE_TRANSFER:" + transfer.getTransferNo() + ":RELABEL", items,
|
||||
trim(transfer.getRelabelDeliveryMethod()), false);
|
||||
firstText(transfer.getRelabelDeliveryMethod(), "TRUCK"), false);
|
||||
return relabelService.createForExistingTailOrder(customerId, operatorId,
|
||||
transfer.getLastlegOrderId(), request);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user