提交
This commit is contained in:
+13
-4
@@ -32,7 +32,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -119,19 +118,29 @@ public class LastlegOrderController {
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/pallet-merge-required/batch")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Mark a selected tail-leg order group for PDA pallet merge")
|
||||
public ApiResponse<Void> setPalletMergeBatch(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody LastlegOrderRequests.SetPalletMergeBatch request) {
|
||||
orderService.setPalletMergeBatch(platformScope(user, customerId), request, operatorName(user));
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EXPORT + "')")
|
||||
@Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EXPORT)
|
||||
public ResponseEntity<StreamingResponseBody> export(
|
||||
public ResponseEntity<byte[]> export(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
byte[] body = orderService.exportCsv(platformScope(user, customerId), query);
|
||||
StreamingResponseBody stream = output -> { output.write(body); output.flush(); };
|
||||
return ResponseEntity.ok()
|
||||
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=lastleg-orders.csv")
|
||||
.body(stream);
|
||||
.body(body);
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/destination")
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ public final class DispatchPdaResponses {
|
||||
public record Task(Long id, String taskNo, String dispatchNo, String vehiclePlate, String warehouseId, String status,
|
||||
List<Item> items) {}
|
||||
public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String fbaWarehouseCode, int scheduledPackages,
|
||||
String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel,
|
||||
int scheduledPackages,
|
||||
long pickedPackages, List<Position> positions) {}
|
||||
public record Position(Long id, Long balanceId, long availablePackages, long allocatedPackages,
|
||||
long pickedPackages) {}
|
||||
|
||||
+8
-1
@@ -225,7 +225,8 @@ public class DispatchPdaWorkflowService {
|
||||
List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of()
|
||||
: positions.listPallet(customerId, item.getWarehousePalletId());
|
||||
values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(),
|
||||
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), item.getPackageCount(),
|
||||
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
|
||||
warehouseRouteLabel(item.getFbaWarehouseCode(), item.getDestinationWarehouseCode()), item.getPackageCount(),
|
||||
picks.totalItem(customerId, task.getId(), item.getId()), palletPositions.stream().map(p ->
|
||||
new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()),
|
||||
n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList()));
|
||||
@@ -268,6 +269,12 @@ public class DispatchPdaWorkflowService {
|
||||
private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException(e); } }
|
||||
private static long n(Long value) { return value == null ? 0 : value; }
|
||||
private static long n(Integer value) { return value == null ? 0 : value; }
|
||||
static String warehouseRouteLabel(String originalFbaWarehouseCode, String destinationWarehouseCode) {
|
||||
String original = trim(originalFbaWarehouseCode);
|
||||
String destination = trim(destinationWarehouseCode);
|
||||
return original != null && destination != null && !original.equalsIgnoreCase(destination)
|
||||
? original + " -> " + destination : null;
|
||||
}
|
||||
private static String trim(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); }
|
||||
private static String required(String value, String message) { String result = trim(value); if (result == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); return result; }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
|
||||
@@ -18,8 +18,8 @@ public final class LastlegOrderRequests {
|
||||
}
|
||||
|
||||
public record Query(
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size,
|
||||
@Min(1) Long page,
|
||||
@Min(1) @Max(200) Long size,
|
||||
String orderNo,
|
||||
String orderType,
|
||||
String inboundCode,
|
||||
@@ -35,8 +35,8 @@ public final class LastlegOrderRequests {
|
||||
LocalDateTime createdTo
|
||||
) {
|
||||
public Query {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
if (page == null || page == 0) page = 1L;
|
||||
if (size == null || size == 0) size = 20L;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,4 +79,10 @@ public final class LastlegOrderRequests {
|
||||
@NotNull Boolean palletMergeRequired
|
||||
) {
|
||||
}
|
||||
|
||||
public record SetPalletMergeBatch(
|
||||
@NotEmpty @Size(max = 200) List<Long> orderIds,
|
||||
@NotNull Boolean palletMergeRequired
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ public record LastlegOrderSummary(
|
||||
String relabelStatus,
|
||||
String palletMergeFlag,
|
||||
String palletMergeBatchId,
|
||||
List<String> palletMergeOrderNos,
|
||||
@Schema(description = "同一合托组内其他来源入仓单的入仓码")
|
||||
List<String> palletMergeInboundNos,
|
||||
String currentStatus,
|
||||
String subOrderStatus,
|
||||
String privateRemark,
|
||||
|
||||
@@ -52,6 +52,7 @@ public class LastlegOrder {
|
||||
private String relabelOrderNo;
|
||||
private String relabelStatus;
|
||||
private String palletMergeFlag;
|
||||
private String palletMergeBatchId;
|
||||
private String currentStatus;
|
||||
private String subOrderStatus;
|
||||
private String privateRemark;
|
||||
|
||||
+3
-2
@@ -26,7 +26,8 @@ public record InboundPlanListResponse(
|
||||
LocalDateTime appointmentAt,
|
||||
LocalDateTime expectedLoadingAt,
|
||||
String dispatchStatus,
|
||||
LocalDateTime createdAt
|
||||
LocalDateTime createdAt,
|
||||
Integer version
|
||||
) {
|
||||
public InboundPlanListResponse(Long lineId, Long planId, String planNo, String customerId,
|
||||
Long orderId, String inboundCode, String orderType,
|
||||
@@ -39,6 +40,6 @@ public record InboundPlanListResponse(
|
||||
this(lineId, planId, planNo, customerId, orderId, inboundCode, orderType, warehouseId,
|
||||
warehouseCode, warehouseName, fbaWarehouseCode, customerName, packageCount, 0,
|
||||
volumeCbm, palletCount, status, appointmentNo, appointmentStatus, appointmentAt,
|
||||
expectedLoadingAt, dispatchStatus, createdAt);
|
||||
expectedLoadingAt, dispatchStatus, createdAt, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -18,15 +18,24 @@ public record InboundPlanResponse(
|
||||
int pickedPackageCount,
|
||||
List<Line> lines,
|
||||
List<Pallet> pallets,
|
||||
List<Allocation> allocations
|
||||
List<Allocation> allocations,
|
||||
int version
|
||||
) {
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> allocations) {
|
||||
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
|
||||
totalVolumeCbm, lines, pallets, allocations, 0);
|
||||
}
|
||||
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> allocations, int version) {
|
||||
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
|
||||
totalVolumeCbm, lines.stream().mapToInt(Line::requiredPackageCount).sum(),
|
||||
allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations);
|
||||
allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations, version);
|
||||
}
|
||||
public record Line(Long id, Long orderId, String inboundCode, String fbaWarehouseCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.entity;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -24,4 +25,6 @@ public class InboundPlan {
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
@Version
|
||||
private Integer version;
|
||||
}
|
||||
|
||||
+3
-6
@@ -23,12 +23,9 @@ public interface InboundPlanOrderMapper {
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,''))) AS destinationWarehouseId," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,''))) AS destinationWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_name,''),MIN(NULLIF(d.destination_warehouse_name,''))) AS destinationWarehouseName," +
|
||||
"COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) AS destinationCount," +
|
||||
"SUM(CASE WHEN d.id IS NOT NULL " +
|
||||
"AND COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL " +
|
||||
"AND COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#') " +
|
||||
"<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " +
|
||||
"AS destinationMismatchCount " +
|
||||
"CASE WHEN COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL THEN 1 " +
|
||||
"ELSE COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) END AS destinationCount," +
|
||||
"0 AS destinationMismatchCount " +
|
||||
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.id=#{orderId} " +
|
||||
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.pallet_count,o.fba_warehouse_code," +
|
||||
|
||||
+3
-2
@@ -45,8 +45,9 @@ public interface InboundPlanQueryMapper {
|
||||
"l.planned_pallet_count palletCount,p.status status,a.appointment_no appointmentNo," +
|
||||
"a.appointment_status appointmentStatus,a.appointment_at appointmentAt," +
|
||||
"a.expected_loading_at expectedLoadingAt," +
|
||||
"CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)" +
|
||||
" THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt" +
|
||||
"CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.order_id=l.order_id" +
|
||||
" <if test='customerId != null and customerId != \"\"'>AND di.customer_id=#{customerId}</if>)" +
|
||||
" THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt,p.version version" +
|
||||
FROM_AND_FILTER + " ORDER BY p.created_at DESC,l.id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InboundPlanListResponse> rows(@Param("customerId") String customerId,
|
||||
@Param("q") InboundPlanRequests.Query query,
|
||||
|
||||
+16
-2
@@ -227,9 +227,22 @@ public class InboundPlanService {
|
||||
if (plan == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
if (!java.util.Set.of("PENDING_ASSIGNMENT", "ASSIGNED").contains(plan.getStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound plan has started and cannot be edited");
|
||||
int currentVersion = plan.getVersion() == null ? 0 : plan.getVersion();
|
||||
if (request.version() != currentVersion)
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound plan version is stale; reload before editing");
|
||||
plan.setOverseasWarehouseId(request.overseasWarehouseId());
|
||||
plan.setUpdatedAt(LocalDateTime.now());
|
||||
planMapper.updateById(plan);
|
||||
// Keep the denormalized destination snapshot used by list queries in sync with the plan.
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getPlanId, plan.getId())
|
||||
.eq(InboundPlanLine::getCustomerId, plan.getCustomerId()));
|
||||
for (InboundPlanLine line : lines) {
|
||||
line.setDestinationWarehouseId(String.valueOf(request.overseasWarehouseId()));
|
||||
if (trimToNull(request.warehouseCode()) != null) line.setDestinationWarehouseCode(request.warehouseCode().trim());
|
||||
if (trimToNull(request.warehouseName()) != null) line.setDestinationWarehouseName(request.warehouseName().trim());
|
||||
lineMapper.updateById(line);
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@@ -342,7 +355,7 @@ public class InboundPlanService {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Inbound code " + order.inboundCode() + " is not ready for an inbound plan");
|
||||
}
|
||||
if (order.destinationCount() != 1 || order.destinationMismatchCount() != 0 || warehouseKey(order) == null) {
|
||||
if (order.destinationCount() != 1 || warehouseKey(order) == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Inbound code " + order.inboundCode()
|
||||
+ " must have one destination warehouse consistent with all cargo details");
|
||||
@@ -388,7 +401,8 @@ public class InboundPlanService {
|
||||
pallet.getActualPackageCount())).toList(),
|
||||
allocations.stream().map(item -> new InboundPlanResponse.Allocation(item.getLineId(), item.getPalletId(),
|
||||
item.getAllocatedVolumeCbm(), item.getPlannedPackageCount(), item.getCountedPackageCount(),
|
||||
item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList());
|
||||
item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList(),
|
||||
plan.getVersion() == null ? 0 : plan.getVersion());
|
||||
}
|
||||
|
||||
private BigDecimal volume(SourceOrderSnapshot order) {
|
||||
|
||||
+5
-4
@@ -85,7 +85,8 @@ public class InventoryQueryController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return csv(service.exportBalances(platformScope(user, customerId), query), "inventory-balances.csv");
|
||||
String scope = platformScope(user, customerId);
|
||||
return csv(output -> service.writeBalancesCsv(scope, query, output), "inventory-balances.csv");
|
||||
}
|
||||
|
||||
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
|
||||
@@ -95,11 +96,11 @@ public class InventoryQueryController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.LedgerQuery query) {
|
||||
return csv(service.exportLedgers(platformScope(user, customerId), query), "inventory-ledger.csv");
|
||||
String scope = platformScope(user, customerId);
|
||||
return csv(output -> service.writeLedgersCsv(scope, query, output), "inventory-ledger.csv");
|
||||
}
|
||||
|
||||
private static ResponseEntity<StreamingResponseBody> csv(byte[] body, String filename) {
|
||||
StreamingResponseBody stream = output -> { output.write(body); output.flush(); };
|
||||
private static ResponseEntity<StreamingResponseBody> csv(StreamingResponseBody stream, String filename) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
|
||||
|
||||
+55
-14
@@ -17,13 +17,14 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class InventoryQueryService {
|
||||
private static final int EXPORT_LIMIT = 10_000;
|
||||
private static final Set<String> BALANCE_STATUSES = Set.of(
|
||||
"AVAILABLE", "HOLD", "ALLOCATED", "PICKED", "IN_TRANSIT", "RECEIVING", "PROCUREMENT");
|
||||
private static final Set<String> POSITION_STATUSES = Set.of(
|
||||
@@ -79,34 +80,74 @@ public class InventoryQueryService {
|
||||
}
|
||||
|
||||
public byte[] exportBalances(String customerId, InventoryQueryRequests.BalanceQuery query) {
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||
output.write(("\uFEFFcustomerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode,destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages,availablePackages,holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
for (InventoryBalance value : mapper.balances(customerId, query, 0, 10_000)) {
|
||||
InventoryBalanceView v = balanceView(value);
|
||||
output.write((String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()), csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.skuCode()), csv(v.productNameZh()), csv(v.productNameEn()), csv(v.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()), csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()), csv(v.procurementPackages())) + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return output.toByteArray();
|
||||
} catch (IOException e) { throw new IllegalStateException("Unable to export inventory balances", e); }
|
||||
}
|
||||
|
||||
public void writeBalancesCsv(String customerId, InventoryQueryRequests.BalanceQuery query, OutputStream output)
|
||||
throws IOException {
|
||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||
List<String> rows = new ArrayList<>();
|
||||
rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
|
||||
output.write(("\uFEFFcustomerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
|
||||
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,skuCode,productNameZh,productNameEn,totalPackages,availablePackages," +
|
||||
"holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages");
|
||||
mapper.balances(customerId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add(
|
||||
"holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
final int batchSize = 1000;
|
||||
long offset = 0;
|
||||
while (true) {
|
||||
List<InventoryBalanceView> batch = mapper.balances(customerId, query, offset, batchSize).stream().map(this::balanceView).toList();
|
||||
if (batch.isEmpty()) break;
|
||||
for (InventoryBalanceView v : batch) output.write((
|
||||
String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()),
|
||||
csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()),
|
||||
csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.skuCode()), csv(v.productNameZh()), csv(v.productNameEn()),
|
||||
csv(v.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()),
|
||||
csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()),
|
||||
csv(v.procurementPackages()))));
|
||||
return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8);
|
||||
csv(v.procurementPackages())) + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
if (batch.size() < batchSize) break;
|
||||
offset += batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] exportLedgers(String customerId, InventoryQueryRequests.LedgerQuery query) {
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
validateDateRange(query);
|
||||
output.write(("\uFEFFoccurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo,inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo,beforePackages,deltaPackages,afterPackages,operatorId,remark\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
for (InventoryLedger value : mapper.ledgers(customerId, query, 0, 10_000)) {
|
||||
InventoryLedgerView v = ledgerView(value);
|
||||
output.write((String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()), csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()), csv(v.statusFrom()), csv(v.statusTo()), csv(v.beforePackages()), csv(v.deltaPackages()), csv(v.afterPackages()), csv(v.operatorId()), csv(v.remark())) + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return output.toByteArray();
|
||||
} catch (IOException e) { throw new IllegalStateException("Unable to export inventory ledger", e); }
|
||||
}
|
||||
|
||||
public void writeLedgersCsv(String customerId, InventoryQueryRequests.LedgerQuery query, OutputStream output)
|
||||
throws IOException {
|
||||
validateDateRange(query);
|
||||
List<String> rows = new ArrayList<>();
|
||||
rows.add("occurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," +
|
||||
output.write(("\uFEFFoccurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," +
|
||||
"inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo," +
|
||||
"beforePackages,deltaPackages,afterPackages,operatorId,remark");
|
||||
mapper.ledgers(customerId, query, 0, EXPORT_LIMIT).stream().map(this::ledgerView).forEach(v -> rows.add(
|
||||
"beforePackages,deltaPackages,afterPackages,operatorId,remark\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
final int batchSize = 1000;
|
||||
long offset = 0;
|
||||
while (true) {
|
||||
List<InventoryLedgerView> batch = mapper.ledgers(customerId, query, offset, batchSize).stream().map(this::ledgerView).toList();
|
||||
if (batch.isEmpty()) break;
|
||||
for (InventoryLedgerView v : batch) output.write((
|
||||
String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()),
|
||||
csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()),
|
||||
csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()),
|
||||
csv(v.statusFrom()), csv(v.statusTo()), csv(v.beforePackages()), csv(v.deltaPackages()),
|
||||
csv(v.afterPackages()), csv(v.operatorId()), csv(v.remark()))));
|
||||
return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8);
|
||||
csv(v.afterPackages()), csv(v.operatorId()), csv(v.remark())) + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
if (batch.size() < batchSize) break;
|
||||
offset += batchSize;
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryBalanceView balanceView(InventoryBalance value) {
|
||||
|
||||
@@ -11,10 +11,18 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
record PalletMergeRelationRow(Long orderId, String relatedOrderNo, String mergeGroupNo) {}
|
||||
record PalletMergeRelationRow(Long orderId, String relatedInboundNo, String mergeGroupNo) {}
|
||||
|
||||
@Select("<script>SELECT relation.order_id AS orderId, related.order_no AS relatedOrderNo, " +
|
||||
@Select("<script>SELECT relation.order_id AS orderId," +
|
||||
"COALESCE(NULLIF(related.inbound_code,''),NULLIF(related.inbound_order_no,''),related.source_order_no) AS relatedInboundNo, " +
|
||||
"relation.merge_group_no AS mergeGroupNo FROM (" +
|
||||
"SELECT marked.customer_id,marked.id AS order_id,related.id AS related_order_id," +
|
||||
"marked.pallet_merge_batch_id AS merge_group_no FROM ll_order marked " +
|
||||
"JOIN ll_order related ON related.customer_id=marked.customer_id " +
|
||||
"AND related.pallet_merge_batch_id=marked.pallet_merge_batch_id AND related.id<>marked.id " +
|
||||
"WHERE marked.pallet_merge_batch_id IS NOT NULL AND marked.id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"UNION ALL " +
|
||||
"SELECT customer_id,source_order_id AS order_id,target_order_id AS related_order_id,merge_group_no " +
|
||||
"FROM ll_inbound_pallet_merge WHERE status='CONFIRMED' AND source_order_id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
@@ -59,9 +67,17 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("warehouseName") String warehouseName);
|
||||
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag},updated_at=#{updatedAt} " +
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag}," +
|
||||
"pallet_merge_batch_id=CASE WHEN #{flag} IS NULL THEN NULL ELSE pallet_merge_batch_id END," +
|
||||
"updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id}")
|
||||
int updatePalletMergeFlag(@Param("customerId") String customerId, @Param("id") long id,
|
||||
@Param("flag") String flag,
|
||||
@Param("updatedAt") java.time.LocalDateTime updatedAt);
|
||||
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag},pallet_merge_batch_id=#{batchId},updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id}")
|
||||
int updatePalletMergeBatch(@Param("customerId") String customerId, @Param("id") long id,
|
||||
@Param("flag") String flag, @Param("batchId") String batchId,
|
||||
@Param("updatedAt") java.time.LocalDateTime updatedAt);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -181,6 +184,46 @@ public class LastlegOrderService {
|
||||
operationLogMapper.insert(operation);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setPalletMergeBatch(String customerId, LastlegOrderRequests.SetPalletMergeBatch request,
|
||||
String operatorName) {
|
||||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (ids.isEmpty()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
if (orders.size() != ids.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
|
||||
}
|
||||
if (orders.stream().map(LastlegOrder::getCustomerId).distinct().count() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Pallet merge orders must belong to one customer");
|
||||
}
|
||||
if (orders.stream().map(order -> StringUtils.hasText(order.getTransferWarehouseCode())
|
||||
? order.getTransferWarehouseCode() : order.getFbaWarehouseCode()).filter(Objects::nonNull).distinct().count() > 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Pallet merge orders must have one destination warehouse");
|
||||
}
|
||||
if (orders.stream().anyMatch(order -> "INBOUND_COMPLETED".equals(order.getCurrentStatus())
|
||||
|| PALLET_MERGED.equals(order.getPalletMergeFlag()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Completed pallet merge orders cannot be changed");
|
||||
}
|
||||
String batchId = request.palletMergeRequired()
|
||||
? "PMG-" + java.util.UUID.randomUUID().toString().replace("-", "") : null;
|
||||
String flag = request.palletMergeRequired() ? PALLET_MERGE_REQUIRED : null;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (LastlegOrder order : orders) {
|
||||
if (orderMapper.updatePalletMergeBatch(order.getCustomerId(), order.getId(), flag, batchId, now) != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet merge group was not updated");
|
||||
}
|
||||
LastlegOrderOperationLog operation = new LastlegOrderOperationLog();
|
||||
operation.setId(IdWorker.getId()); operation.setCustomerId(order.getCustomerId());
|
||||
operation.setOrderId(order.getId());
|
||||
operation.setOperationNode(request.palletMergeRequired() ? "批量标记合托" : "取消合托标记");
|
||||
operation.setOperatorName(operatorName);
|
||||
operation.setResult(request.palletMergeRequired() ? "PDA合托分组 " + batchId : "已取消PDA合托分组");
|
||||
operation.setCreatedAt(now); operationLogMapper.insert(operation);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
|
||||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||||
@@ -236,12 +279,25 @@ public class LastlegOrderService {
|
||||
}
|
||||
|
||||
public byte[] exportCsv(String customerId, LastlegOrderRequests.Query query) {
|
||||
List<LastlegOrder> orders = orderMapper.selectList(filter(customerId, query)
|
||||
.orderByDesc(LastlegOrder::getCreatedAt).last("LIMIT 10000"));
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)");
|
||||
for (LastlegOrder order : orders) {
|
||||
lines.add(String.join(",", List.of(
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
writeExportCsv(customerId, query, output);
|
||||
return output.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to export tail-leg orders", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeExportCsv(String customerId, LastlegOrderRequests.Query query, OutputStream output)
|
||||
throws IOException {
|
||||
output.write("\uFEFFID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,换标单号,换标状态,合托标记,主流程状态,子单状态,备注(仅自己),备注(客户),备注(仓库)\r\n".getBytes(StandardCharsets.UTF_8));
|
||||
final int batchSize = 1000;
|
||||
long page = 1;
|
||||
while (true) {
|
||||
List<LastlegOrder> orders = orderMapper.selectPage(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, batchSize),
|
||||
filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId)).getRecords();
|
||||
if (orders.isEmpty()) break;
|
||||
for (LastlegOrder order : orders) {
|
||||
String line = String.join(",", List.of(
|
||||
csv(order.getId()), csv(order.getOrderNo()), csv(LastlegOrderType.labelOf(order.getOrderType())),
|
||||
csv(order.getInboundCode()),
|
||||
csv(order.getSourceOrderNo()), csv(order.getCustomerName()), csv(order.getOverseasWarehouseName()),
|
||||
@@ -251,9 +307,13 @@ public class LastlegOrderService {
|
||||
csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()),
|
||||
csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getRelabelOrderNo()), csv(order.getRelabelStatus()),
|
||||
csv(order.getPalletMergeFlag()), csv(order.getCurrentStatus()), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()),
|
||||
csv(order.getCustomerRemark()), csv(order.getWarehouseRemark()))));
|
||||
csv(order.getCustomerRemark()), csv(order.getWarehouseRemark()))) + "\r\n";
|
||||
output.write(line.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
output.flush();
|
||||
if (orders.size() < batchSize) break;
|
||||
page++;
|
||||
}
|
||||
return ("\uFEFF" + String.join("\r\n", lines)).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
|
||||
@@ -320,7 +380,7 @@ public class LastlegOrderService {
|
||||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getRelabelOrderNo(), o.getRelabelStatus(),
|
||||
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
|
||||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedOrderNo).filter(Objects::nonNull).distinct().toList(),
|
||||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(),
|
||||
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
|
||||
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user