fix(firstleg): preserve sealed pallet identity in atomic PDA picking
This commit is contained in:
@@ -59,11 +59,12 @@ public final class LoadingPickDtos {
|
||||
|
||||
public record BatchItemRequest(
|
||||
@NotNull @Min(1) Long itemId,
|
||||
@NotBlank @Pattern(regexp = "PICK|PARTIAL|SKIP") String action,
|
||||
@NotBlank @Pattern(regexp = "PICK|PARTIAL|SKIP|PICK_PALLET") String action,
|
||||
@Min(1) Integer pickedPackageCount,
|
||||
@Min(1) Integer finalPickedPackageCount,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@Size(max = 500) String reason
|
||||
@Size(max = 500) String reason,
|
||||
@Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -98,6 +99,7 @@ public final class LoadingPickDtos {
|
||||
|
||||
|
||||
public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId,
|
||||
String unitType, Long palletId, String palletNo, Boolean indivisible,
|
||||
String orderNo, String inboundCode, String cargoName, List<String> boxCodes,
|
||||
List<InventoryResource> inventoryResources,
|
||||
Integer plannedPackageCount, Integer pickedPackageCount,
|
||||
@@ -152,6 +154,10 @@ public final class LoadingPickDtos {
|
||||
public record InventorySnapshot(Long taskId, Long loadingPlanId, Long containerBookingId,
|
||||
String cabinetNo, Integer taskVersion, String currentContainerNo,
|
||||
Long warehouseId, LocalDateTime capturedAt, String snapshotToken,
|
||||
List<InboundInventory> inbounds) {
|
||||
List<InboundInventory> inbounds, List<InventoryUnit> units) {
|
||||
}
|
||||
|
||||
public record InventoryUnit(Long pickItemId, Long loadingPlanItemId, Long inboundId,
|
||||
String unitType, Long palletId, String palletNo, Boolean indivisible) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
@@ -18,6 +19,10 @@ public class LoadingPickItemEntity {
|
||||
private Long id;
|
||||
private Long pickTaskId;
|
||||
private Long loadingPlanItemId;
|
||||
@TableField(exist = false) private String unitType;
|
||||
@TableField(exist = false) private Long palletId;
|
||||
@TableField(exist = false) private String palletNo;
|
||||
@TableField(exist = false) private Boolean indivisible;
|
||||
private Long inboundId;
|
||||
private Long inboundDetailId;
|
||||
private Integer plannedPackageCount;
|
||||
|
||||
@@ -11,6 +11,17 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity> {
|
||||
@Select("""
|
||||
SELECT pick_item.*, plan_item.unit_type, plan_item.pallet_id, plan_item.pallet_no, plan_item.indivisible
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task task ON task.id = pick_item.pick_task_id
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id
|
||||
AND plan_item.loading_plan_id = task.loading_plan_id
|
||||
WHERE pick_item.pick_task_id = #{taskId}
|
||||
ORDER BY pick_item.id ASC
|
||||
""")
|
||||
List<LoadingPickItemEntity> selectTaskItemsWithPlan(@Param("taskId") long taskId);
|
||||
|
||||
@Select("""
|
||||
SELECT pick_item.id AS pickItemId, plan_item.account_id AS tenantId, plan_item.order_no AS orderNo,
|
||||
plan_item.inbound_code AS inboundCode, label.cargo_name AS cargoName,
|
||||
@@ -133,7 +144,7 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
record InboundOwner(Long tenantId, Long inboundId) {}
|
||||
|
||||
@Select("""
|
||||
SELECT pick_item.*
|
||||
SELECT pick_item.*, plan_item.unit_type, plan_item.pallet_id, plan_item.pallet_no, plan_item.indivisible
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id
|
||||
@@ -150,7 +161,7 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
SELECT pick_item.*
|
||||
SELECT pick_item.*, plan_item.unit_type, plan_item.pallet_id, plan_item.pallet_no, plan_item.indivisible
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id
|
||||
|
||||
@@ -105,9 +105,7 @@ public class LoadingPickRepository {
|
||||
}
|
||||
|
||||
public List<LoadingPickItemEntity> items(long taskId) {
|
||||
return items.selectList(new LambdaQueryWrapper<LoadingPickItemEntity>()
|
||||
.eq(LoadingPickItemEntity::getPickTaskId, taskId)
|
||||
.orderByAsc(LoadingPickItemEntity::getId));
|
||||
return items.selectTaskItemsWithPlan(taskId);
|
||||
}
|
||||
|
||||
public List<CartonLabelRow> cartonLabels(long taskId) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryResource;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InboundInventory;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryUnit;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
||||
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||
@@ -484,11 +485,11 @@ public class LoadingPickService {
|
||||
|
||||
List<BatchLine> lines = validateBatchLines(task, request);
|
||||
validateInventorySnapshot(task, request, lines);
|
||||
lines.forEach(line -> requirePackageUnit(line.item()));
|
||||
lines.stream().filter(line -> !"PICK_PALLET".equals(line.action())).forEach(line -> requirePackageUnit(line.item()));
|
||||
for (BatchLine line : lines) {
|
||||
applyBatchLine(taskId, line, operationId, operatorId, operatorName);
|
||||
}
|
||||
if (lines.stream().anyMatch(line -> "PICK".equals(line.action()) || "PARTIAL".equals(line.action()))) {
|
||||
if (lines.stream().anyMatch(line -> "PICK".equals(line.action()) || "PARTIAL".equals(line.action()) || "PICK_PALLET".equals(line.action()))) {
|
||||
startPickingIfNeeded(task, operatorId, operatorName);
|
||||
}
|
||||
if (!repository.allTaskItemsResolved(taskId)) {
|
||||
@@ -968,7 +969,8 @@ public class LoadingPickService {
|
||||
Long tenantId = first == null ? tenantByInbound.get(item.getInboundId()) : first.getTenantId();
|
||||
return new PickItem(item.getId(), item.getLoadingPlanItemId(), tenantId,
|
||||
item.getInboundId(),
|
||||
item.getInboundDetailId(), first == null ? null : first.getOrderNo(),
|
||||
item.getInboundDetailId(), item.getUnitType(), item.getPalletId(), item.getPalletNo(), item.getIndivisible(),
|
||||
first == null ? null : first.getOrderNo(),
|
||||
first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(),
|
||||
cartons.stream().map(CartonLabelRow::getBoxCode).toList(),
|
||||
inventoryByInbound.getOrDefault(item.getInboundId(), List.of()), item.getPlannedPackageCount(),
|
||||
@@ -1045,7 +1047,22 @@ public class LoadingPickService {
|
||||
throw conflict("PDA明细版本已变化,请刷新后重试");
|
||||
}
|
||||
String action = requestItem.action().trim().toUpperCase();
|
||||
if ("PICK".equals(action)) {
|
||||
boolean palletUnit = "SEALED_PALLET".equals(item.getUnitType()) || item.getPalletId() != null || Boolean.TRUE.equals(item.getIndivisible());
|
||||
if (palletUnit && !"PICK_PALLET".equals(action)) {
|
||||
throw conflict("一件代发已封托货物必须整托提交,不能普通拣货、部分拣货或跳拣");
|
||||
}
|
||||
if (!"PICK_PALLET".equals(action) && requestItem.palletNo() != null) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "普通箱件动作不能携带托盘码");
|
||||
}
|
||||
if ("PICK_PALLET".equals(action)) {
|
||||
if (!"SEALED_PALLET".equals(item.getUnitType()) || item.getPalletId() == null
|
||||
|| !Boolean.TRUE.equals(item.getIndivisible()) || !StringUtils.hasText(item.getPalletNo())
|
||||
|| !item.getPalletNo().equals(requestItem.palletNo())) throw conflict("托盘码与当前装柜整托单元不一致");
|
||||
if (requestItem.pickedPackageCount() != null || requestItem.finalPickedPackageCount() != null
|
||||
|| StringUtils.hasText(requestItem.reason())) throw new BizException(ResultCode.BAD_REQUEST, "整托拣货不能携带箱数或跳拣原因");
|
||||
if (!"PENDING".equals(item.getItemStatus()) || count(item.getPickedPackageCount()) != 0)
|
||||
throw conflict("托盘已有部分作业,禁止拆托处理");
|
||||
} else if ("PICK".equals(action)) {
|
||||
requireNoUnexpectedBatchFields(requestItem, false, true);
|
||||
int current = count(item.getPickedPackageCount());
|
||||
int planned = count(item.getPlannedPackageCount());
|
||||
@@ -1075,14 +1092,35 @@ public class LoadingPickService {
|
||||
throw conflict("同一入库单已有拣货记录,不能在整批中跳拣");
|
||||
}
|
||||
} else {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "action只能是PICK、PARTIAL或SKIP");
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "action只能是PICK、PARTIAL、SKIP或PICK_PALLET");
|
||||
}
|
||||
result.add(new BatchLine(item, requestItem, action, request.warehouseId()));
|
||||
}
|
||||
validateWholePallets(result, scopedItems, request.warehouseId());
|
||||
result.sort(Comparator.comparing(line -> line.item().getId()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void validateWholePallets(List<BatchLine> lines, List<LoadingPickItemEntity> allItems, long warehouseId) {
|
||||
Map<Long, List<BatchLine>> groups = lines.stream().filter(line -> "PICK_PALLET".equals(line.action()))
|
||||
.collect(Collectors.groupingBy(line -> line.item().getPalletId()));
|
||||
for (var entry : groups.entrySet()) {
|
||||
List<LoadingPickItemEntity> palletItems = allItems.stream().filter(item -> entry.getKey().equals(item.getPalletId())).toList();
|
||||
if (palletItems.size() != entry.getValue().size()) throw conflict("整托提交必须包含托盘全部待拣明细,不得拆托");
|
||||
var physical = dropshipPalletizationMapper.sealedPalletLines(entry.getKey());
|
||||
String palletNo = entry.getValue().get(0).item().getPalletNo();
|
||||
if (palletItems.stream().anyMatch(item -> !palletNo.equals(item.getPalletNo()))) throw conflict("托盘装柜快照不一致");
|
||||
if (physical.isEmpty() || physical.stream().anyMatch(line -> !"SEALED".equals(line.getPalletStatus())
|
||||
|| count(line.getCartonCount()) < 1 || !Objects.equals(line.getWarehouseId(), warehouseId) || !palletNo.equals(line.getPalletNo())))
|
||||
throw conflict("托盘已失效、尚未封托或不属于当前仓库");
|
||||
Map<String, Integer> physicalCounts = new HashMap<>();
|
||||
physical.forEach(line -> physicalCounts.merge(line.getInboundId() + ":" + line.getInboundDetailId(), count(line.getCartonCount()), Integer::sum));
|
||||
Map<String, Integer> plannedCounts = new HashMap<>();
|
||||
palletItems.forEach(item -> plannedCounts.merge(item.getInboundId() + ":" + item.getInboundDetailId(), count(item.getPlannedPackageCount()), Integer::sum));
|
||||
if (!physicalCounts.equals(plannedCounts)) throw conflict("整托计划明细与封托箱数不一致,禁止部分拣货");
|
||||
}
|
||||
}
|
||||
|
||||
private List<LoadingPickItemEntity> warehouseScopedItems(LoadingPickTaskEntity task, long warehouseId) {
|
||||
List<LoadingPickItemEntity> allItems = repository.items(task.getId());
|
||||
List<LoadingPickItemEntity> scopedItems = repository.items(task.getId(), warehouseId);
|
||||
@@ -1102,11 +1140,14 @@ public class LoadingPickService {
|
||||
int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum();
|
||||
return new InboundInventory(inboundId, available, resources);
|
||||
}).toList();
|
||||
String token = inventoryToken(task, warehouseId, inbounds);
|
||||
List<InventoryUnit> units = items.stream().sorted(Comparator.comparing(LoadingPickItemEntity::getId))
|
||||
.map(item -> new InventoryUnit(item.getId(), item.getLoadingPlanItemId(), item.getInboundId(),
|
||||
item.getUnitType(), item.getPalletId(), item.getPalletNo(), item.getIndivisible())).toList();
|
||||
String token = inventoryToken(task, warehouseId, inbounds, units);
|
||||
var booking = smartLoadingRepository.booking(task.getContainerBookingId());
|
||||
return new InventorySnapshot(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(), task.getCabinetNo(),
|
||||
task.getVersion(), booking == null ? null : booking.getContainerNo(), warehouseId,
|
||||
LocalDateTime.now(), token, inbounds);
|
||||
LocalDateTime.now(), token, inbounds, units);
|
||||
}
|
||||
|
||||
private Map<Long, Long> tenantByInbound(long taskId) {
|
||||
@@ -1160,7 +1201,7 @@ public class LoadingPickService {
|
||||
}
|
||||
|
||||
private static String inventoryToken(LoadingPickTaskEntity task, long warehouseId,
|
||||
List<InboundInventory> inbounds) {
|
||||
List<InboundInventory> inbounds, List<InventoryUnit> units) {
|
||||
StringBuilder canonical = new StringBuilder("INVENTORY|").append(task.getId()).append('|')
|
||||
.append(task.getVersion()).append('|').append(warehouseId);
|
||||
for (InboundInventory inbound : inbounds) {
|
||||
@@ -1171,6 +1212,11 @@ public class LoadingPickService {
|
||||
.append('|').append(resource.availablePackageCount());
|
||||
}
|
||||
}
|
||||
for (InventoryUnit unit : units) {
|
||||
canonical.append("|UNIT|").append(unit.pickItemId()).append('|').append(unit.loadingPlanItemId())
|
||||
.append('|').append(unit.inboundId()).append('|').append(unit.unitType())
|
||||
.append('|').append(unit.palletId()).append('|').append(unit.palletNo()).append('|').append(unit.indivisible());
|
||||
}
|
||||
return hash(canonical.toString());
|
||||
}
|
||||
|
||||
@@ -1191,6 +1237,7 @@ public class LoadingPickService {
|
||||
int picked = line == null ? count(item.getPickedPackageCount())
|
||||
: "PICK".equals(line.action())
|
||||
? count(item.getPickedPackageCount()) + line.request().pickedPackageCount()
|
||||
: "PICK_PALLET".equals(line.action()) ? count(item.getPlannedPackageCount())
|
||||
: "PARTIAL".equals(line.action()) ? line.request().finalPickedPackageCount() : 0;
|
||||
requestedByInbound.merge(item.getInboundId(), picked, Integer::sum);
|
||||
}
|
||||
@@ -1207,8 +1254,8 @@ public class LoadingPickService {
|
||||
BatchItemRequest request = line.request();
|
||||
int changed;
|
||||
int pickedCount;
|
||||
if ("PICK".equals(line.action())) {
|
||||
pickedCount = request.pickedPackageCount();
|
||||
if ("PICK".equals(line.action()) || "PICK_PALLET".equals(line.action())) {
|
||||
pickedCount = "PICK_PALLET".equals(line.action()) ? count(item.getPlannedPackageCount()) : request.pickedPackageCount();
|
||||
changed = repository.pick(item.getId(), pickedCount, request.version(), line.warehouseId());
|
||||
} else if ("PARTIAL".equals(line.action())) {
|
||||
pickedCount = request.finalPickedPackageCount();
|
||||
@@ -1222,7 +1269,7 @@ public class LoadingPickService {
|
||||
requireNoTaskOperation(itemOperationId);
|
||||
if (repository.history(itemOperationId) != null) throw conflict("整批明细已被重复提交");
|
||||
String itemHash = hash(line.action() + '|' + item.getId() + '|' + request.version()
|
||||
+ '|' + pickedCount + '|' + trim(request.reason()));
|
||||
+ '|' + pickedCount + '|' + trim(request.reason()) + '|' + request.palletNo());
|
||||
insertHistory(taskId, item.getId(), itemOperationId, itemHash, line.action(), pickedCount,
|
||||
trim(request.reason()), operatorId, operatorName);
|
||||
}
|
||||
@@ -1373,7 +1420,7 @@ public class LoadingPickService {
|
||||
canonical.append('|').append(item.itemId()).append('|').append(trim(item.action()))
|
||||
.append('|').append(item.pickedPackageCount()).append('|')
|
||||
.append(item.finalPickedPackageCount()).append('|')
|
||||
.append(item.version()).append('|').append(trim(item.reason()));
|
||||
.append(item.version()).append('|').append(trim(item.reason())).append('|').append(item.palletNo());
|
||||
});
|
||||
return hash(canonical.toString());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user