StockAllocatorImpl.java
/*
* Copyright 2005-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openwms.wms.inventory.allocation.spi;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.NotLogged;
import org.ameba.annotation.NotTransformed;
import org.ameba.annotation.TxService;
import org.openwms.core.units.api.Measurable;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.PackagingUnitFinder;
import org.openwms.wms.inventory.allocation.AllocationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static java.util.Arrays.asList;
import static org.openwms.wms.InventoryLoggerCategories.ALLOCATION;
/**
* A StockAllocationImpl.
*
* @author Heiko Scherrer
*/
@NotLogged
@Validated
@TxService
class StockAllocatorImpl implements Allocator {
private static final Logger ALLOCATION_LOGGER = LoggerFactory.getLogger(ALLOCATION);
private final PackagingUnitFinder packagingUnitFinder;
StockAllocatorImpl(PackagingUnitFinder packagingUnitFinder) {
this.packagingUnitFinder = packagingUnitFinder;
}
/**
* {@inheritDoc}
*/
@NotTransformed
@Measured
@Override
public List<PackagingUnit> allocate(@Valid @NotNull AllocationRule allocationRule) throws AllocationException {
var sourceLocationGroupNames = allocationRule.getSourceLocationGroupNames() == null || allocationRule.getSourceLocationGroupNames().isEmpty()
? Collections.emptyList()
: allocationRule.getSourceLocationGroupNames();
var packagingUnitsOnLocation = packagingUnitFinder
.findAvailablesForProductOnLocationsOnly(allocationRule.getProduct()).stream()
.filter(pu -> sourceLocationGroupNames.isEmpty() || sourceLocationGroupNames.contains(pu.getActualLocation().getLocationGroup()))
.toList();
var packagingUnitsInLoadUnit = packagingUnitFinder
.findAvailablesForProductInLoadUnitsOnly(allocationRule.getProduct()).stream()
.filter(pu -> sourceLocationGroupNames.isEmpty() || sourceLocationGroupNames.contains(pu.getLoadUnit().getTransportUnit().getActualLocation().getLocationGroup()))
.toList();
var packagingUnits = new ArrayList<>(packagingUnitsOnLocation);
packagingUnits.addAll(packagingUnitsInLoadUnit);
if (packagingUnits.isEmpty()) {
ALLOCATION_LOGGER.info("Could not allocate product [{}] in LocationGroups {}", allocationRule.getProduct(), sourceLocationGroupNames);
throw new AllocationException("No available PackagingUnits found for allocation");
} else {
if (ALLOCATION_LOGGER.isDebugEnabled()) {
ALLOCATION_LOGGER.debug("[{}] number of PackagingUnits exist for Product [{}]", packagingUnits.size(), allocationRule.getProduct());
}
}
List<PackagingUnit> result = new ArrayList<>(0);
Measurable quantityDemand = allocationRule.getQuantity();
for (var packagingUnit : packagingUnits) {
if (quantityDemand.isZero() || quantityDemand.isNegative()) {
ALLOCATION_LOGGER.debug("Demanded quantity is satisfied");
break;
}
var qtyAvailable = packagingUnit.getQtyAvailable();
if (!qtyAvailable.isZero() && !qtyAvailable.isNegative()) {
result.add(packagingUnit);
quantityDemand = quantityDemand.subtract(qtyAvailable);
if (ALLOCATION_LOGGER.isDebugEnabled()) {
ALLOCATION_LOGGER.trace("Potential PackagingUnit [{}] with quantity [{}] added to result list, new quantityDemand is [{}]", packagingUnit.getPersistentKey(), packagingUnit.getQuantity(), quantityDemand);
}
} else {
ALLOCATION_LOGGER.trace("SKIPPING: Not enough [{}] unreserved quantity available on PackagingUnit [{}]", qtyAvailable, packagingUnit.getPersistentKey());
}
}
if (result.isEmpty()) {
ALLOCATION_LOGGER.debug("There are other PackagingUnits to check, [{}] eaches", packagingUnits.size());
Optional<PackagingUnit> smallest = packagingUnits.stream()
.filter(pu -> !pu.getQtyAvailable().isZero() && !pu.getQtyAvailable().isNegative())
.min((a, b) -> a.getQuantity().compareTo(b.getQuantity()));
if (smallest.isPresent()) {
result = asList(smallest.get());
ALLOCATION_LOGGER.debug("FOUND: Return the smallest one: [{}]", result);
} else {
result = Collections.emptyList();
ALLOCATION_LOGGER.debug("BREAK: But all of them are already reserved, one of those is [{}]", packagingUnits.get(0));
}
}
return result;
}
}