提交
This commit is contained in:
@@ -16527,6 +16527,7 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
||||
AND UPPER(role.role_code) IN ('PLATFORM_ADMIN','PLATFORM_CUSTOM_ADMIN')
|
||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||
|
||||
-- PLATFORM_CUSTOM_ADMIN needs access to the overseas outbound and delivery lists shown in the admin navigation.
|
||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, role.id, permission.id, NOW(3)
|
||||
FROM baiyue_iam.iam_role role
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ class OverseasPhase4PermissionSqlContractTest {
|
||||
assertThat(sql).contains(REQUIRED.toArray(String[]::new));
|
||||
assertThat(sql).contains("'lastleg:pda:operate'");
|
||||
assertPdaPickerGrant(sql);
|
||||
assertThat(catalog).hasSize(54).noneMatch(code -> code.endsWith(":view"));
|
||||
assertThat(catalog).hasSize(56).noneMatch(code -> code.endsWith(":view"));
|
||||
assertThat(sql).contains("permission_code LIKE 'platform:lastleg:%'", "status='INACTIVE'", "deleted=1");
|
||||
assertThat(sql).doesNotContain(":carton:", ":product:");
|
||||
assertThat(catalog).contains("platform:overseas-warehouse:delivery:list");
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
- 列表、详情、导出和更新必须使用同一租户边界,不能仅在列表过滤而让详情或更新越权。
|
||||
- 目的仓变更必须写状态/操作历史或审计证据,并使用版本控制防止并发覆盖。
|
||||
- 货物废弃只使用可核对 SKU 和箱唛的一件代发库存。创建申请只能将箱从 `AVAILABLE` 锁定为 `DISCARD_PENDING`,不得扣减 SKU 箱数;实际扣库存节点固定为具备 `lastleg:cargo-discard:approve` 权限的仓库/PDA 审核人确认废弃时,并且必须在审核事务内调用专用 discard adjustment。该 adjustment 必须将箱改为 `DISCARDED`、写逐箱业务流水及唯一幂等键,并校验 `扣减前箱数 - 废弃箱数 = 扣减后箱数` 且逐箱数等于申请总箱数;任一环节失败须整体回滚。拒绝申请只释放锁箱,不得扣库存。
|
||||
- 海外采购客户下单固定进入 `PENDING_REVIEW`;四期采购只维护采购订单、审核、入库数量、候选快照和操作流水,不提供采购出库端点、出库记录或出库数量。采购单据不得写入或冒充 `available_packages`;只有海外仓 PDA 完成实收后,`InboundStockReceiptService` 才能以 `PDA_STOCK_IN:{taskId}:{allocationId}` 幂等键调用库存 `stockIn` 转入可用库存。禁止通过通用平台接口绕过 PDA 入可用库存。
|
||||
- 海外采购客户下单固定进入 `PENDING_REVIEW`;采购编辑可维护入库数量和采购出库明细,出库量为明细数量汇总,审核通过且有入库量后按 `PENDING_OUTBOUND`、`PARTIALLY_OUTBOUND`、`OUTBOUNDED` 派生展示状态。采购出库只记录采购单据流转,不得写入或冒充 `available_packages`;只有海外仓 PDA 完成实收后,`InboundStockReceiptService` 才能以 `PDA_STOCK_IN:{taskId}:{allocationId}` 幂等键调用库存 `stockIn` 转入可用库存。禁止通过通用平台接口绕过 PDA 入可用库存。
|
||||
- 派车状态只允许 PDA 拣货、完成和出库动作推进,普通派车编辑接口不得修改状态。PDA 写操作要求 `lastleg:pda:operate`;后台库存表单操作要求 `lastleg:inventory:manage`,盘点调整另要求 `lastleg:inventory:adjust`,转仓库存操作要求 `lastleg:warehouse-transfer:manage`。
|
||||
- `ll_order.current_status` 只保存尾程主流程状态;换标、转仓、出库和派送等子单精确状态保存到各自单据并汇总到 `sub_order_status`,不得再用子单状态覆盖主流程状态。PDA 入仓及库存落账完成后,主流程状态固定推进为 `INBOUND_COMPLETED`。
|
||||
|
||||
|
||||
+4
-2
@@ -32,6 +32,7 @@ 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;
|
||||
@@ -121,15 +122,16 @@ public class LastlegOrderController {
|
||||
@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<byte[]> export(
|
||||
public ResponseEntity<StreamingResponseBody> 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(body);
|
||||
.body(stream);
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/destination")
|
||||
|
||||
+3
-3
@@ -13,8 +13,8 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@@ -78,8 +78,8 @@ public class DeliveryTransferController {
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DELIVERY_LIST + "')")
|
||||
public ApiResponse<DeliveryResponses.DeliveryDetail> deliveryDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@PathVariable String deliveryNo, @Valid DeliveryRequests.PalletQuery query) {
|
||||
return ApiResponse.success(service.deliveryDetail(scope(user, customerId), deliveryNo, query));
|
||||
@PathVariable String deliveryNo) {
|
||||
return ApiResponse.success(service.deliveryDetail(scope(user, customerId), deliveryNo));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
|
||||
+2
-3
@@ -33,9 +33,8 @@ public class MerchantDeliveryController {
|
||||
|
||||
@GetMapping("/{deliveryNo}")
|
||||
public ApiResponse<DeliveryResponses.DeliveryDetail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String deliveryNo,
|
||||
@Valid DeliveryRequests.PalletQuery query) {
|
||||
return ApiResponse.success(service.deliveryDetail(customer(user), deliveryNo, query));
|
||||
@PathVariable String deliveryNo) {
|
||||
return ApiResponse.success(service.deliveryDetail(customer(user), deliveryNo));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
|
||||
+2
-3
@@ -341,8 +341,7 @@ public class DeliveryTransferService {
|
||||
return page(page, page.getRecords().stream().map(this::deliveryRow).toList());
|
||||
}
|
||||
|
||||
public DeliveryResponses.DeliveryDetail deliveryDetail(String customerId, String deliveryNo,
|
||||
DeliveryRequests.PalletQuery query) {
|
||||
public DeliveryResponses.DeliveryDetail deliveryDetail(String customerId, String deliveryNo) {
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("Delivery order not found");
|
||||
List<DispatchItem> items = dispatchItems.findByDispatchOrder(customerId, delivery.getDispatchOrderId());
|
||||
@@ -398,7 +397,7 @@ public class DeliveryTransferService {
|
||||
}
|
||||
private DeliveryResponses.OutboundRow outboundRow(OutboundOrder o) { return new DeliveryResponses.OutboundRow(o.getId(), o.getOutboundNo(), o.getDispatchNo(), o.getOverseasWarehouseId(), o.getStatus(), o.getPalletCount(), o.getPackageCount(), o.getCreatedAt()); }
|
||||
private DeliveryResponses.DeliveryRow deliveryRow(DeliveryOrder d) { return new DeliveryResponses.DeliveryRow(d.getId(), d.getCustomerId(), d.getCustomerName(), d.getDeliveryNo(), d.getOutboundNo(), d.getDispatchNo(), d.getOverseasWarehouseId(), d.getOverseasWarehouseName(), d.getStatus(), d.getWaybillNo(), d.getDeliveryMethod(), d.getCarrierName(), d.getPickupMethod(), d.getLoadingType(), d.getVehicleType(), d.getVehicleSize(), d.getVehiclePlate(), d.getDriverName(), d.getDriverPhone(), d.getAppointmentAccount(), d.getSellerName(), value(d.getPalletCount()), value(d.getPackageCount()), d.getTotalWeightKg(), d.getTotalVolumeCbm(), d.getPlannedDeliveryAt(), d.getDispatchedAt(), d.getExpectedArrivalAt(), d.getSignedAt(), d.getRemark(), d.getCreatedAt()); }
|
||||
private DeliveryResponses.DeliverySku sku(LastlegOrderDetail d, LastlegOrder order) { return new DeliveryResponses.DeliverySku(order == null ? null : order.getInboundCode(), order == null ? null : order.getCustomerName(), d.getDestinationWarehouseCode(), d.getDestinationWarehouseName(), d.getShipmentNo(), d.getFbaTrackingNo(), d.getSkuCode(), firstText(d.getProductNameZh(), d.getProductNameEn()), value(d.getPackageCount()), d.getGrossWeightKg(), d.getVolumeCbm()); }
|
||||
private DeliveryResponses.DeliverySku sku(LastlegOrderDetail d, LastlegOrder order) { return new DeliveryResponses.DeliverySku(order == null ? null : order.getInboundCode(), order == null ? null : order.getCustomerName(), d.getDestinationWarehouseCode(), null, d.getShipmentNo(), d.getFbaTrackingNo(), d.getSkuCode(), firstText(d.getProductNameZh(), d.getProductNameEn()), value(d.getPackageCount()), d.getGrossWeightKg(), d.getVolumeCbm()); }
|
||||
private DeliveryResponses.DeliveryFeeRow feeRow(DeliveryFee f) { return new DeliveryResponses.DeliveryFeeRow(f.getId(), f.getFeeDirection(), f.getFeeType(), f.getCurrency(), f.getAmount(), f.getRemark()); }
|
||||
private DeliveryResponses.DeliveryOperationRow operationRow(DeliveryOperation o) { return new DeliveryResponses.DeliveryOperationRow(o.getId(), o.getCreatedAt(), o.getOperationNode(), firstText(o.getOperatorName(), o.getOperatorId()), o.getResult()); }
|
||||
private void recordOperation(String customerId, Long deliveryId, String operatorId, String node, String result, LocalDateTime now) { DeliveryOperation operation = new DeliveryOperation(); operation.setCustomerId(customerId); operation.setDeliveryOrderId(deliveryId); operation.setOperationNode(node); operation.setOperatorId(operatorId); operation.setOperatorName(operatorId); operation.setResult(result); operation.setCreatedAt(now); deliveryOperations.insert(operation); }
|
||||
|
||||
+6
-8
@@ -119,6 +119,11 @@ public class DispatchPdaWorkflowService {
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":PICK", request.remark()));
|
||||
// A successful PDA scan is the inventory deduction point. Departure only
|
||||
// changes outbound/dispatch facts and must not deduct the same quantity again.
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":SHIP", request.remark()));
|
||||
DispatchPdaPick pick = new DispatchPdaPick();
|
||||
pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId());
|
||||
pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages());
|
||||
@@ -197,14 +202,7 @@ public class DispatchPdaWorkflowService {
|
||||
}
|
||||
List<DispatchPdaPick> dispatchPicks = picks.listTask(customerId, task.getId());
|
||||
if (dispatchPicks.isEmpty()) throw conflict("Dispatch has no PDA picks to ship");
|
||||
long shipped = 0;
|
||||
for (DispatchPdaPick pick : dispatchPicks) {
|
||||
String key = "DISPATCH:" + order.getDispatchNo() + ":PICK:" + pick.getId() + ":SHIP";
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(
|
||||
pick.getInventoryPositionId(), n(pick.getPackages()),
|
||||
outbound.getOutboundNo(), outbound.getOutboundNo(), null, key, request.remark()));
|
||||
shipped += n(pick.getPackages());
|
||||
}
|
||||
long shipped = dispatchPicks.stream().mapToLong(p -> n(p.getPackages())).sum();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
pickAttribution.depart(customerId, outbound.getOutboundNo(), order.getDispatchNo(), dispatchPicks, now);
|
||||
outbound.setStatus("OUTBOUNDED");
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "按入仓单编码粒度展示的尾程订单列表行")
|
||||
public record LastlegOrderSummary(
|
||||
@@ -40,6 +41,8 @@ public record LastlegOrderSummary(
|
||||
String relabelOrderNo,
|
||||
String relabelStatus,
|
||||
String palletMergeFlag,
|
||||
String palletMergeBatchId,
|
||||
List<String> palletMergeOrderNos,
|
||||
String currentStatus,
|
||||
String subOrderStatus,
|
||||
String privateRemark,
|
||||
|
||||
+10
@@ -11,6 +11,7 @@ import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
@@ -62,6 +63,15 @@ public class InboundPlanController {
|
||||
return ApiResponse.success(service.detailByPlanNo(scope(user, customerId), planNo));
|
||||
}
|
||||
|
||||
@PutMapping("/{planNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||
@Operation(summary = "编辑入仓计划")
|
||||
public ApiResponse<InboundPlanResponse> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@Valid @RequestBody UpdateInboundPlanRequest request) {
|
||||
return ApiResponse.success(service.update(scope(user, request.customerId()), planNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{planNo}/pallets/supplement")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||
/** 为计划目标仓新增一个可打印的补充托盘。 */
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record UpdateInboundPlanRequest(
|
||||
@NotBlank String customerId,
|
||||
@NotNull Long overseasWarehouseId,
|
||||
String warehouseCode,
|
||||
String warehouseName,
|
||||
@NotNull Integer version
|
||||
) {}
|
||||
+14
@@ -7,6 +7,7 @@ import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
@@ -219,6 +220,19 @@ public class InboundPlanService {
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse update(String customerId, String planNo, UpdateInboundPlanRequest request) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getPlanNo, planNo).eq(customerId != null, InboundPlan::getCustomerId, customerId).last("FOR UPDATE"));
|
||||
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");
|
||||
plan.setOverseasWarehouseId(request.overseasWarehouseId());
|
||||
plan.setUpdatedAt(LocalDateTime.now());
|
||||
planMapper.updateById(plan);
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse.Pallet supplementPallet(String customerId, String planNo, SupplementPalletRequest request) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
|
||||
+6
-4
@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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;
|
||||
@@ -80,7 +81,7 @@ public class InventoryQueryController {
|
||||
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EXPORT + "')")
|
||||
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
|
||||
public ResponseEntity<byte[]> exportBalances(
|
||||
public ResponseEntity<StreamingResponseBody> exportBalances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
@@ -90,18 +91,19 @@ public class InventoryQueryController {
|
||||
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INVENTORY_EXPORT + "')")
|
||||
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV", description = "Required authority: " + OverseasPermissions.INVENTORY_EXPORT)
|
||||
public ResponseEntity<byte[]> exportLedgers(
|
||||
public ResponseEntity<StreamingResponseBody> exportLedgers(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.LedgerQuery query) {
|
||||
return csv(service.exportLedgers(platformScope(user, customerId), query), "inventory-ledger.csv");
|
||||
}
|
||||
|
||||
private static ResponseEntity<byte[]> csv(byte[] body, String filename) {
|
||||
private static ResponseEntity<StreamingResponseBody> csv(byte[] body, String filename) {
|
||||
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=" + filename)
|
||||
.body(body);
|
||||
.body(stream);
|
||||
}
|
||||
|
||||
private String platformScope(AuthenticatedUser user, String requestedCustomerId) {
|
||||
|
||||
@@ -7,8 +7,24 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
record PalletMergeRelationRow(Long orderId, String relatedOrderNo, String mergeGroupNo) {}
|
||||
|
||||
@Select("<script>SELECT relation.order_id AS orderId, related.order_no AS relatedOrderNo, " +
|
||||
"relation.merge_group_no AS mergeGroupNo FROM (" +
|
||||
"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> " +
|
||||
"UNION ALL SELECT customer_id,target_order_id AS order_id,source_order_id AS related_order_id,merge_group_no " +
|
||||
"FROM ll_inbound_pallet_merge WHERE status='CONFIRMED' AND target_order_id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach>) relation " +
|
||||
"JOIN ll_order related ON related.id=relation.related_order_id AND related.customer_id=relation.customer_id " +
|
||||
"ORDER BY relation.order_id,relation.merge_group_no,related.order_no</script>")
|
||||
List<PalletMergeRelationRow> findPalletMergeRelations(@Param("orderIds") List<Long> orderIds);
|
||||
|
||||
@Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND source_region=#{sourceRegion} " +
|
||||
"AND source_inbound_id=#{sourceInboundId} LIMIT 1")
|
||||
LastlegOrder findSource(@Param("customerId") String customerId,
|
||||
|
||||
@@ -31,7 +31,9 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@@ -53,7 +55,8 @@ public class LastlegOrderService {
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||
new Page<>(query.page(), query.size()), filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt));
|
||||
return new PageResult<>(page.getRecords().stream().map(this::summary).toList(),
|
||||
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(page.getRecords());
|
||||
return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()))).toList(),
|
||||
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
@@ -75,7 +78,7 @@ public class LastlegOrderService {
|
||||
.eq(LastlegOrderOperationLog::getCustomerId, order.getCustomerId())
|
||||
.eq(LastlegOrderOperationLog::getOrderId, orderId)
|
||||
.orderByAsc(LastlegOrderOperationLog::getCreatedAt));
|
||||
return new LastlegOrderDetailResponse(summary(order), cargo, history.stream()
|
||||
return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId())), cargo, history.stream()
|
||||
.map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(),
|
||||
value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream()
|
||||
.map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(),
|
||||
@@ -294,7 +297,20 @@ public class LastlegOrderService {
|
||||
}
|
||||
}
|
||||
|
||||
private LastlegOrderSummary summary(LastlegOrder o) {
|
||||
private Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> palletMergeRelations(List<LastlegOrder> orders) {
|
||||
if (orders.isEmpty()) return Map.of();
|
||||
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> result = new LinkedHashMap<>();
|
||||
List<LastlegOrderMapper.PalletMergeRelationRow> rows = orderMapper.findPalletMergeRelations(
|
||||
orders.stream().map(LastlegOrder::getId).toList());
|
||||
if (rows == null) return Map.of();
|
||||
for (LastlegOrderMapper.PalletMergeRelationRow row : rows) {
|
||||
result.computeIfAbsent(row.orderId(), ignored -> new ArrayList<>()).add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private LastlegOrderSummary summary(LastlegOrder o, List<LastlegOrderMapper.PalletMergeRelationRow> relations) {
|
||||
List<LastlegOrderMapper.PalletMergeRelationRow> related = relations == null ? List.of() : relations;
|
||||
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
|
||||
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
||||
o.getInboundCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
||||
@@ -303,7 +319,9 @@ public class LastlegOrderService {
|
||||
o.getAbnormal(), o.getDetained(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
|
||||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||||
o.getEstimatedWarehouseAt(), o.getInboundOrderNo(), o.getRelabelOrderNo(), o.getRelabelStatus(),
|
||||
o.getPalletMergeFlag(), o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
|
||||
o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null),
|
||||
related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedOrderNo).filter(Objects::nonNull).distinct().toList(),
|
||||
o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(),
|
||||
o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt());
|
||||
}
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ components:
|
||||
feeItem: { type: string, minLength: 1, maxLength: 128 }
|
||||
currency: { type: string, pattern: '^[A-Z]{3}$', example: USD }
|
||||
amount: { type: number, format: decimal, minimum: 0.01 }
|
||||
status: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED, PENDING_OUTBOUND, PARTIALLY_OUTBOUND, OUTBOUNDED] }
|
||||
status: { type: string, enum: [PENDING_REVIEW, APPROVED, REJECTED] }
|
||||
supplierDescription: { type: string, maxLength: 500 }
|
||||
LastMileFee:
|
||||
type: object
|
||||
|
||||
+4
-1
@@ -23,6 +23,9 @@ class OverseasProcurementInventoryBoundaryTest {
|
||||
assertFalse(source.contains("availablePackages"));
|
||||
assertFalse(source.contains("addOutbound"));
|
||||
assertFalse(source.contains("OverseasProcurementOutbound"));
|
||||
assertFalse(source.contains("outboundQuantity"));
|
||||
String responses = Files.readString(Path.of(
|
||||
"src/main/java/com/baiyue/lastleg/procurement/dto/OverseasProcurementResponses.java"),
|
||||
StandardCharsets.UTF_8);
|
||||
assertTrue(responses.contains("outboundQuantity"));
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -64,9 +64,32 @@ class OverseasProcurementServiceTest {
|
||||
|
||||
assertEquals(OverseasProcurementService.PENDING_REVIEW, detail.summary().status());
|
||||
assertEquals(0, detail.summary().inboundQuantity());
|
||||
assertEquals(0, detail.summary().outboundQuantity());
|
||||
assertEquals(0, detail.summary().inventoryQuantity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundQuantityAndDisplayStatusFollowOutboundDetails() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.APPROVED, 90, 0);
|
||||
when(orders.findNo("100", "HC1")).thenReturn(order);
|
||||
when(histories.listOrder("100", 1L)).thenReturn(List.of());
|
||||
|
||||
order.setOutboundDetails("[]");
|
||||
assertEquals("PENDING_OUTBOUND", service.detail("100", "HC1").summary().status());
|
||||
|
||||
order.setOutboundDetails("[{\"id\":\"1\",\"quantity\":1}]");
|
||||
var partial = service.detail("100", "HC1").summary();
|
||||
assertEquals(1, partial.outboundQuantity());
|
||||
assertEquals(89, partial.inventoryQuantity());
|
||||
assertEquals("PARTIALLY_OUTBOUND", partial.status());
|
||||
|
||||
order.setOutboundDetails("[{\"id\":\"1\",\"quantity\":90}]");
|
||||
var completed = service.detail("100", "HC1").summary();
|
||||
assertEquals(90, completed.outboundQuantity());
|
||||
assertEquals(0, completed.inventoryQuantity());
|
||||
assertEquals("OUTBOUNDED", completed.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingOrderCannotReceiveInventory() {
|
||||
OverseasProcurementOrder order = order(OverseasProcurementService.PENDING_REVIEW, 0, 0);
|
||||
|
||||
@@ -193,6 +193,26 @@ class LastlegOrderServiceTest {
|
||||
assertEquals(25, page.getValue().getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageReturnsConfirmedPalletMergeRelatedOrderNumbers() {
|
||||
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IB-1");
|
||||
order.setPalletMergeFlag("MERGED");
|
||||
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
|
||||
resultPage.setRecords(List.of(order));
|
||||
resultPage.setTotal(1);
|
||||
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
||||
when(orders.findPalletMergeRelations(List.of(11L))).thenReturn(List.of(
|
||||
new LastlegOrderMapper.PalletMergeRelationRow(11L, "LMO-RELATED-2", "PMG-1"),
|
||||
new LastlegOrderMapper.PalletMergeRelationRow(11L, "LMO-RELATED-3", "PMG-1")));
|
||||
var query = new LastlegOrderRequests.Query(1, 10, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null);
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
assertEquals("PMG-1", result.records().get(0).palletMergeBatchId());
|
||||
assertEquals(List.of("LMO-RELATED-2", "LMO-RELATED-3"), result.records().get(0).palletMergeOrderNos());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdingBeforeInboundPersistsTheOrderMarkerAndOperationRemark() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_INBOUND", null);
|
||||
|
||||
Reference in New Issue
Block a user