Merge branch 'dev-lyhfive' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev
This commit is contained in:
+150
-18
@@ -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;
|
||||
@@ -33,10 +34,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class InboundPlanService {
|
||||
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private InboundPlanMapper planMapper;
|
||||
@Resource private InboundPlanLineMapper lineMapper;
|
||||
@Resource private InboundPlanPalletMapper palletMapper;
|
||||
@@ -61,6 +64,37 @@ public class InboundPlanService {
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||
return convert(customerId, selectedOrderId, List.of());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos) {
|
||||
return convert(customerId, selectedOrderId, sourcePalletNos, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convertDropship(String customerId, Long selectedOrderId,
|
||||
List<WholePalletInput> sourcePallets) {
|
||||
if (sourcePallets == null || sourcePallets.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship inbound requires source pallets");
|
||||
}
|
||||
HashSet<String> palletNos = new HashSet<>();
|
||||
for (WholePalletInput pallet : sourcePallets) {
|
||||
if (pallet == null || trimToNull(pallet.palletNo()) == null || pallet.packageCount() <= 0) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship source pallet number and package count are required");
|
||||
}
|
||||
if (!palletNos.add(pallet.palletNo().trim())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Duplicate one-click dropship source pallet number: " + pallet.palletNo());
|
||||
}
|
||||
}
|
||||
return convert(customerId, selectedOrderId, List.of(), List.copyOf(sourcePallets));
|
||||
}
|
||||
|
||||
private InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets) {
|
||||
if (customerId == null || customerId.isBlank() || "0".equals(customerId.trim())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "平台生成入库计划必须指定客户");
|
||||
}
|
||||
@@ -75,16 +109,17 @@ public class InboundPlanService {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
return createPlan(customerId, selectedOrderId, orders);
|
||||
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets);
|
||||
}
|
||||
|
||||
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
|
||||
List<SourceOrderSnapshot> orders) {
|
||||
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets) {
|
||||
String containerNo = requiredContainerNo(orders.get(0).containerNo());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(IdWorker.getId());
|
||||
plan.setPlanNo("LIP" + plan.getId());
|
||||
plan.setPlanNo(orderNoGenerator.next("INBOUND_PLAN", orders.get(0).destinationWarehouseCode(), plan.getId()));
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(selectedOrderId);
|
||||
plan.setContainerNo(containerNo);
|
||||
@@ -98,20 +133,27 @@ public class InboundPlanService {
|
||||
Map<Long, SourceOrderSnapshot> orderById = orders.stream()
|
||||
.collect(Collectors.toMap(SourceOrderSnapshot::orderId, value -> value));
|
||||
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
|
||||
PalletAllocator.Result allocation = PalletAllocator.allocate(orders.stream()
|
||||
PalletAllocator.Result allocation = wholePallets == null ? PalletAllocator.allocate(orders.stream()
|
||||
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
|
||||
order.packageCount()))
|
||||
.toList());
|
||||
plan.setPalletCount(allocation.pallets().size());
|
||||
order.packageCount(), order.recommendedPalletCount())).toList()) : null;
|
||||
if (wholePallets != null) validateWholePallets(orders, wholePallets);
|
||||
plan.setPalletCount(wholePallets == null ? allocation.pallets().size() : wholePallets.size());
|
||||
planMapper.insert(plan);
|
||||
|
||||
lineByOrder.values().forEach(lineMapper::insert);
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation, orderById);
|
||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||
|
||||
Map<Long, Integer> palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
||||
PalletAllocator.AllocationDraft::orderId,
|
||||
Collectors.collectingAndThen(Collectors.mapping(item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
||||
Map<Long, Integer> palletCounts;
|
||||
if (wholePallets == null) {
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation,
|
||||
orderById, sourcePalletNos == null ? List.of() : sourcePalletNos);
|
||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||
palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
||||
PalletAllocator.AllocationDraft::orderId, Collectors.collectingAndThen(Collectors.mapping(
|
||||
item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
||||
} else {
|
||||
SourceOrderSnapshot order = orders.get(0);
|
||||
createWholePallets(plan, order, lineByOrder.get(order.orderId()), wholePallets);
|
||||
palletCounts = Map.of(order.orderId(), wholePallets.size());
|
||||
}
|
||||
for (SourceOrderSnapshot order : orders) {
|
||||
int count = palletCounts.getOrDefault(order.orderId(), 0);
|
||||
InboundPlanLine line = lineByOrder.get(order.orderId());
|
||||
@@ -180,7 +222,7 @@ public class InboundPlanService {
|
||||
List<InboundPlanResponse> created = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index)));
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(), null));
|
||||
}
|
||||
return List.copyOf(created);
|
||||
}
|
||||
@@ -201,6 +243,53 @@ public class InboundPlanService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateWholePallets(List<SourceOrderSnapshot> orders, List<WholePalletInput> pallets) {
|
||||
if (orders.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship inbound plan must belong to one inbound order");
|
||||
}
|
||||
int packages = pallets.stream().mapToInt(WholePalletInput::packageCount).sum();
|
||||
if (packages != orders.get(0).packageCount()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship pallet package total must equal inbound order package count");
|
||||
}
|
||||
}
|
||||
|
||||
private void createWholePallets(InboundPlan plan, SourceOrderSnapshot order, InboundPlanLine line,
|
||||
List<WholePalletInput> sourcePallets) {
|
||||
BigDecimal remainingVolume = volume(order);
|
||||
int remainingPackages = order.packageCount();
|
||||
for (int index = 0; index < sourcePallets.size(); index++) {
|
||||
WholePalletInput source = sourcePallets.get(index);
|
||||
BigDecimal palletVolume = index == sourcePallets.size() - 1 ? remainingVolume
|
||||
: volume(order).multiply(BigDecimal.valueOf(source.packageCount()))
|
||||
.divide(BigDecimal.valueOf(order.packageCount()), 4, java.math.RoundingMode.HALF_UP);
|
||||
if (palletVolume.compareTo(remainingVolume) > 0) palletVolume = remainingVolume;
|
||||
InboundPlanPallet pallet = new InboundPlanPallet();
|
||||
pallet.setId(IdWorker.getId()); pallet.setCustomerId(plan.getCustomerId()); pallet.setPlanId(plan.getId());
|
||||
pallet.setPalletNo(source.palletNo().trim()); pallet.setDestinationWarehouseId(order.destinationWarehouseId());
|
||||
pallet.setDestinationWarehouseCode(order.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(order.destinationWarehouseName());
|
||||
pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM); pallet.setAllocatedVolumeCbm(palletVolume);
|
||||
pallet.setSequenceNo(index + 1); pallet.setAssignmentStatus("UNASSIGNED");
|
||||
pallet.setActualVolumeCbm(BigDecimal.ZERO); pallet.setActualPackageCount(0); palletMapper.insert(pallet);
|
||||
|
||||
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation();
|
||||
allocation.setId(IdWorker.getId()); allocation.setCustomerId(plan.getCustomerId()); allocation.setPlanId(plan.getId());
|
||||
allocation.setLineId(line.getId()); allocation.setPalletId(pallet.getId());
|
||||
allocation.setAllocatedVolumeCbm(palletVolume); allocation.setPlannedPackageCount(source.packageCount());
|
||||
allocation.setCountedPackageCount(0); allocation.setPickedPackageCount(0);
|
||||
allocation.setActualVolumeCbm(BigDecimal.ZERO); allocation.setStatus("PENDING_COUNT"); allocation.setVersion(0);
|
||||
allocationMapper.insert(allocation);
|
||||
remainingVolume = remainingVolume.subtract(palletVolume);
|
||||
remainingPackages -= source.packageCount();
|
||||
}
|
||||
if (remainingPackages != 0 || remainingVolume.signum() != 0) {
|
||||
throw new IllegalStateException("Failed to conserve one-click dropship pallet totals");
|
||||
}
|
||||
}
|
||||
|
||||
public record WholePalletInput(String palletNo, int packageCount) {}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||
@@ -214,6 +303,44 @@ 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");
|
||||
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);
|
||||
}
|
||||
// Pallets carry the same destination snapshot and are consumed by PDA/resource assignment.
|
||||
// Update them in this transaction so a plan edit cannot leave lines and pallets pointing at
|
||||
// different warehouses.
|
||||
List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
.eq(InboundPlanPallet::getPlanId, plan.getId())
|
||||
.eq(InboundPlanPallet::getCustomerId, plan.getCustomerId()));
|
||||
for (InboundPlanPallet pallet : pallets) {
|
||||
pallet.setDestinationWarehouseId(String.valueOf(request.overseasWarehouseId()));
|
||||
if (trimToNull(request.warehouseCode()) != null) pallet.setDestinationWarehouseCode(request.warehouseCode().trim());
|
||||
if (trimToNull(request.warehouseName()) != null) pallet.setDestinationWarehouseName(request.warehouseName().trim());
|
||||
palletMapper.updateById(pallet);
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse.Pallet supplementPallet(String customerId, String planNo, SupplementPalletRequest request) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
@@ -265,7 +392,8 @@ public class InboundPlanService {
|
||||
}
|
||||
|
||||
private Map<PalletAllocator.PalletDraft, InboundPlanPallet> createPallets(
|
||||
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById) {
|
||||
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById,
|
||||
List<String> sourcePalletNos) {
|
||||
Map<String, SourceOrderSnapshot> warehouseSamples = orderById.values().stream()
|
||||
.collect(Collectors.toMap(this::warehouseKey, value -> value, (left, right) -> left));
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> result = new HashMap<>();
|
||||
@@ -275,7 +403,10 @@ public class InboundPlanService {
|
||||
pallet.setId(IdWorker.getId());
|
||||
pallet.setCustomerId(plan.getCustomerId());
|
||||
pallet.setPlanId(plan.getId());
|
||||
pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", result.size() + 1));
|
||||
int palletIndex = result.size();
|
||||
String sourcePalletNo = palletIndex < sourcePalletNos.size() ? sourcePalletNos.get(palletIndex) : null;
|
||||
pallet.setPalletNo(sourcePalletNo == null || sourcePalletNo.isBlank()
|
||||
? plan.getPlanNo() + "-P" + String.format("%03d", palletIndex + 1) : sourcePalletNo.trim());
|
||||
pallet.setDestinationWarehouseId(sample.destinationWarehouseId());
|
||||
pallet.setDestinationWarehouseCode(sample.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(sample.destinationWarehouseName());
|
||||
@@ -319,7 +450,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");
|
||||
@@ -365,7 +496,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) {
|
||||
|
||||
+22
-1
@@ -34,6 +34,23 @@ public final class PalletAllocator {
|
||||
PalletDraft current = null;
|
||||
int sequence = 0;
|
||||
for (LineInput line : group) {
|
||||
if (line.recommendedPalletCount() > 0) {
|
||||
int minimumByVolume = line.volumeCbm().divide(PALLET_CAPACITY_CBM, 0, RoundingMode.CEILING).intValue();
|
||||
int count = Math.max(line.recommendedPalletCount(), minimumByVolume);
|
||||
BigDecimal remaining = line.volumeCbm();
|
||||
for (int index = 0; index < count; index++) {
|
||||
PalletDraft recommended = new PalletDraft(line.warehouseKey(), ++sequence, BigDecimal.ZERO);
|
||||
pallets.add(recommended);
|
||||
int remainingPallets = count - index;
|
||||
BigDecimal allocated = remainingPallets == 1 ? remaining
|
||||
: remaining.divide(BigDecimal.valueOf(remainingPallets), 4, RoundingMode.HALF_UP);
|
||||
recommended.add(allocated);
|
||||
allocations.add(new AllocationDraft(line.orderId(), recommended, allocated, 0));
|
||||
remaining = remaining.subtract(allocated);
|
||||
}
|
||||
current = null;
|
||||
continue;
|
||||
}
|
||||
BigDecimal remaining = line.volumeCbm();
|
||||
while (remaining.signum() > 0) {
|
||||
if (current == null || current.remainingCapacity().signum() == 0) {
|
||||
@@ -85,7 +102,11 @@ public final class PalletAllocator {
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
|
||||
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount,
|
||||
int recommendedPalletCount) {
|
||||
public LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
|
||||
this(orderId, warehouseKey, volumeCbm, packageCount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PalletDraft {
|
||||
|
||||
Reference in New Issue
Block a user