PackagingUnitServiceImpl.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.impl;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.TxService;
import org.ameba.integration.jpa.ApplicationEntity;
import org.apache.commons.lang3.SerializationUtils;
import org.openwms.core.units.api.Measurable;
import org.openwms.core.units.api.Piece;
import org.openwms.values.Message;
import org.openwms.wms.CycleAvoidingMappingContext;
import org.openwms.wms.inventory.LoadUnit;
import org.openwms.wms.inventory.LoadUnitService;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.PackagingUnitFinder;
import org.openwms.wms.inventory.PackagingUnitMapper;
import org.openwms.wms.inventory.PackagingUnitService;
import org.openwms.wms.inventory.Product;
import org.openwms.wms.inventory.ProductService;
import org.openwms.wms.inventory.ReservationService;
import org.openwms.wms.inventory.api.AvailabilityState;
import org.openwms.wms.inventory.api.DeletePURequestVO;
import org.openwms.wms.inventory.api.PackagingUnitVO;
import org.openwms.wms.inventory.api.ReportProblemVO;
import org.openwms.wms.inventory.events.PackagingUnitEvent;
import org.openwms.wms.location.Location;
import org.openwms.wms.location.LocationPK;
import org.openwms.wms.location.LocationService;
import org.openwms.wms.location.api.LocationVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.List;
import static java.lang.String.format;
import static org.openwms.wms.inventory.events.PackagingUnitEvent.TYPE.UNLOCKED;
/**
* A PackagingUnitServiceImpl.
*
* @author Heiko Scherrer
*/
@Validated
@TxService
class PackagingUnitServiceImpl implements PackagingUnitService {
private static final Logger LOGGER = LoggerFactory.getLogger(PackagingUnitServiceImpl.class);
private final ApplicationEventPublisher eventPublisher;
private final PackagingUnitMapper packagingUnitMapper;
private final PackagingUnitRepository repository;
private final PackagingUnitFinder packagingUnitFinder;
private final ReservationService reservationService;
private final LocationService locationService;
private final LoadUnitService loadUnitService;
private final ProductService productService;
PackagingUnitServiceImpl(ApplicationEventPublisher eventPublisher, PackagingUnitMapper packagingUnitMapper,
PackagingUnitRepository repository, PackagingUnitFinder packagingUnitFinder, ReservationService reservationService,
LocationService locationService, LoadUnitService loadUnitService, ProductService productService) {
this.eventPublisher = eventPublisher;
this.packagingUnitMapper = packagingUnitMapper;
this.repository = repository;
this.packagingUnitFinder = packagingUnitFinder;
this.reservationService = reservationService;
this.locationService = locationService;
this.loadUnitService = loadUnitService;
this.productService = productService;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void acknowledgeReservation(@NotBlank String reservationId, @NotBlank String acknowledgeId) {
reservationService.acknowledgeReservation(reservationId, acknowledgeId);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull PackagingUnitVO update(@NotBlank String pKey, @NotNull PackagingUnitVO packagingUnit) {
var existing = findByPKeyInternal(pKey);
if (existing.hasLoadUnit()) {
if (!packagingUnit.hasLoadUnit()) {
// Moving from a LoadUnit to a Location
if (!packagingUnit.hasActualLocation()) {
throw new IllegalArgumentException(format("Not allowed to move the PackagingUnit [%s] out of a LoadUnit to nowhere", pKey));
}
var sourceLoadUnit = existing.getLoadUnit();
sourceLoadUnit.getPackagingUnits().remove(existing);
var newLocation = packagingUnit.actualLocation.getErpCode() == null
? locationService.findByBK(LocationPK.fromString(packagingUnit.actualLocation.getLocationId()))
: locationService.findByErpCode(packagingUnit.actualLocation.getErpCode());
LOGGER.debug("Moving PackagingUnit [{}] from LoadUnit [{}] to Location [{}]", existing.getPersistentKey(),
sourceLoadUnit.getPersistentKey(), newLocation.getErpCode());
existing.unbindFromLoadUnit(newLocation);
eventPublisher.publishEvent(new PackagingUnitEvent(existing, PackagingUnitEvent.TYPE.MOVED, sourceLoadUnit));
} else if (!existing.getLoadUnit().getPersistentKey().equals(packagingUnit.loadUnit.getpKey())) {
// Moving to different LoadUnit
var targetLoadUnit = getLoadUnit(packagingUnit.loadUnit.getTransportUnitBK(), packagingUnit.loadUnit.physicalPosition);
var sourceLoadUnit = existing.getLoadUnit();
LOGGER.debug("Moving PackagingUnit [{}] from LoadUnit [{}] to LoadUnit [{}]", existing.getPersistentKey(),
sourceLoadUnit.getPersistentKey(), targetLoadUnit.getPersistentKey());
sourceLoadUnit.removePackagingUnits(existing);
targetLoadUnit.addPackagingUnit(existing);
eventPublisher.publishEvent(new PackagingUnitEvent(existing, PackagingUnitEvent.TYPE.MOVED, sourceLoadUnit));
} else {
LOGGER.debug("LoadUnit of PackagingUnit [{}] hasn't changed and is still [{}]", pKey, existing.getLoadUnit().getPersistentKey());
}
} else {
if (!packagingUnit.hasLoadUnit()) {
// Moving from a Location to a Location
if (!packagingUnit.hasActualLocation()) {
throw new IllegalArgumentException(format("Not allowed to move the PackagingUnit [%s] from a Location to nowhere", pKey));
}
var newLocation = packagingUnit.actualLocation.getErpCode() == null
? locationService.findByBK(LocationPK.fromString(packagingUnit.actualLocation.getLocationId()))
: locationService.findByErpCode(packagingUnit.actualLocation.getErpCode());
if (newLocation.equals(existing.getActualLocation())) {
LOGGER.debug("PackagingUnit [{}] is on the same Location [{}]. No updated needed", pKey, newLocation);
} else {
var sourceLocation = existing.getActualLocation();
existing.setActualLocation(newLocation);
eventPublisher.publishEvent(new PackagingUnitEvent(existing, PackagingUnitEvent.TYPE.MOVED, sourceLocation));
}
} else {
// Move from Location to LoadUnit
var targetLoadUnit = getLoadUnit(packagingUnit.loadUnit.getTransportUnitBK(), packagingUnit.loadUnit.getPhysicalPosition());
var sourceLocation = existing.getActualLocation();
LOGGER.debug("Moving PackagingUnit [{}] from Location [{}] to LoadUnit [{}] on TransportUnit [{}]/[{}]",
existing.getPersistentKey(),
sourceLocation.getErpCode(),
targetLoadUnit.getPersistentKey(),
targetLoadUnit.getTransportUnit().getTransportUnitBK(),
targetLoadUnit.getTransportUnit().getPersistentKey());
targetLoadUnit.addPackagingUnit(existing);
eventPublisher.publishEvent(new PackagingUnitEvent(existing, PackagingUnitEvent.TYPE.MOVED, sourceLocation));
}
}
existing = repository.save(existing);
return packagingUnitMapper.convert(existing, new CycleAvoidingMappingContext());
}
private LoadUnit getLoadUnit(String transportUnitBK, String loadUnitPosition) {
return loadUnitService.find(transportUnitBK, loadUnitPosition);
}
private PackagingUnit findByPKeyInternal(String pKey) {
return packagingUnitFinder.findByPKey(pKey);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public @NotNull PackagingUnit save(@NotNull PackagingUnit packagingUnit) {
return repository.save(packagingUnit);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void reportProblem(@NotBlank String transportUnitBK, @NotBlank String loadUnitPosition, @NotNull ReportProblemVO problem) {
LOGGER.debug("Reported problem [{}] on TransportUnit [{}] and LoadUnit [{}]", problem, transportUnitBK, loadUnitPosition);
LoadUnit loadUnit = getLoadUnit(transportUnitBK, loadUnitPosition);
if (problem.getProduct() == null || problem.getProduct().getSku() == null || problem.getProduct().getSku().isEmpty()) {
// Issue that the client send no product!
// TODO [openwms]: 21.09.21 Better check this with BV
throw new IllegalArgumentException("Reported product must not be null");
}
List<PackagingUnit> packagingUnits = loadUnit.getPackagingUnits().stream()
.filter(pu -> pu.getProduct().getSku().equals(problem.getProduct().getSku()))
.sorted((o1, o2) -> o1.getQuantity().compareTo(o2.getQuantity()))
.toList();
Measurable quantity = null;
List<PackagingUnit> selected = new ArrayList<>();
for (int i = 0; i < packagingUnits.size(); i++) {
PackagingUnit pu = packagingUnits.get(i);
if (quantity == null) {
if (pu.getQuantity().compareTo(problem.getQuantity()) <= 0 || (i+1 == packagingUnits.size())) {
// Either the quantity matches or we don't have any PU left then grab that one
quantity = pu.getQuantity();
selected.add(pu);
LOGGER.debug("The PU quantity fits the problem quantity, new collected quantity is [{}]", quantity);
} else {
LOGGER.debug("Continue, PU quantity is too big");
}
} else if (quantity.compareTo(problem.getQuantity()) < 0) {
selected.add(pu);
quantity = quantity.add(pu.getQuantity());
LOGGER.debug("Add PU to collected, collected quantity is [{}]", quantity);
} else {
LOGGER.debug("Collected enough [{}] and break", quantity);
break;
}
}
LOGGER.debug("Collected [{}] PackagingUnits to lock", selected.size());
List<PackagingUnit> updated = selected.stream().map(pu -> {
pu.setAvailabilityState(AvailabilityState.LOCKED);
pu.setMessage(new Message(problem.getMessageNo(), "" + problem.getMessageNo()));
LOGGER.info("Locked PackagingUnit [{}] with messageNo [{}]", pu.getPersistentKey(), problem.getMessageNo());
return repository.saveAndFlush(pu); // FIXME [openwms]: 21.09.21 Is this required to flush here?
}).toList();
updated = repository.saveAll(updated);
updated.forEach(pu -> eventPublisher.publishEvent(new PackagingUnitEvent(pu, PackagingUnitEvent.TYPE.LOCKED)));
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void release(@NotBlank String pKey) {
var lockedPU = findByPKeyInternal(pKey);
if (lockedPU.getAvailabilityState() != AvailabilityState.AVAILABLE) {
lockedPU.setAvailabilityState(AvailabilityState.AVAILABLE);
eventPublisher.publishEvent(new PackagingUnitEvent(lockedPU, UNLOCKED));
}
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void deleteAll(@NotEmpty List<PackagingUnit> packagingUnits) {
LOGGER.debug("Deleting PackagingUnits [{}]", packagingUnits.stream().map(ApplicationEntity::getPersistentKey).toList());
packagingUnits.forEach(this::deleteInternal);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void deleteOnLocation(@NotNull DeletePURequestVO request) {
LOGGER.debug("Delete PackagingUnits on Location. Request: [{}]", request);
deleteOnLocationInternal(request);
}
private void deleteOnLocationInternal(DeletePURequestVO request) {
if (request.hasQuantity()) {
LOGGER.debug("Delete PackagingUnits with a quantity of [{}] from [{}]", request.getQuantity(), request.getLocation());
productService.findBySKUorThrow(request.getProduct().getSku());
delete(packagingUnitFinder.findOnLocation(request.getLocation().getErpCode()), request.getProduct().getSku(), request.getQuantity());
} else {
var location = resolve(request.getLocation());
LOGGER.debug("Delete all PackagingUnits from [{}]", location);
packagingUnitFinder.findOnLocation(location.getErpCode()).forEach(this::deleteInternal);
}
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void deleteByPKey(@NotBlank String pKey) {
deleteInternal(findByPKeyInternal(pKey));
}
private Location resolve(LocationVO location) {
if (location.hasErpCode()) {
return locationService.findByErpCode(location.getErpCode());
} else if (location.hasLocationId()) {
return locationService.findByBK(LocationPK.fromString(location.getLocationId()));
} else {
throw new IllegalArgumentException("Either the ERP code or the Location ID must be given");
}
}
class Collector {
Measurable sum = Piece.ZERO;
List<PackagingUnit> pus = new ArrayList<>();
void add(PackagingUnit pu) {
this.pus.add(pu);
Measurable sz = sum.add(pu.getQuantity());
sum = sz;
}
boolean isEmpty() {
return this.pus.isEmpty();
}
}
void delete(List<PackagingUnit> pus, String sku, Measurable quantity) {
if (pus.isEmpty()) {
LOGGER.warn("No PackagingUnits to delete");
return;
}
final var result = new Collector();
var in = new ArrayList<>(pus);
in.stream()
.filter(pu -> pu.getProduct() != null && pu.getProduct().getSku().equals(sku))
.sorted((o1, o2) -> o1.getQuantity().compareTo(o2.getQuantity()))
.forEach(pu -> {
if (result.sum.add(pu.getQuantity()).compareTo(quantity) <= 0) {
result.add(pu);
}
});
if (!result.isEmpty()) {
LOGGER.debug("Collected PackagingUnits to delete [{}]", result.pus);
result.pus.forEach(this::deleteInternal);
}
// Check if more need to be deleted
if (result.sum.compareTo(quantity) < 0) {
LOGGER.debug("Need to split PU to hit the demanded quantity");
var demandedQty = quantity.subtract(result.sum);
// Split PU
var all = in.stream()
.filter(pu -> pu.getProduct() != null && pu.getProduct().getSku().equals(sku))
.filter(pu -> !result.pus.contains(pu))
.sorted((o1, o2) -> o1.getQuantity().compareTo(o2.getQuantity()))
.toList();
for (var packagingUnit : all) {
var diff = packagingUnit.getQuantity().compareTo(demandedQty);
if (diff > 0) {
var oldQuantity = packagingUnit.getQuantity();
var newQuantity = oldQuantity.subtract(demandedQty);
LOGGER.debug("newQuantity [{}]", newQuantity);
if (newQuantity.isZero() || newQuantity.isNegative()) {
throw new IllegalStateException(format("Expected to have enough on PackagingUnit [%s] but was only [%s]", quantity,
oldQuantity));
}
packagingUnit.setQuantity(newQuantity);
eventPublisher.publishEvent(new PackagingUnitEvent(packagingUnit, PackagingUnitEvent.TYPE.QUANTITY_CHANGED, oldQuantity));
repository.save(packagingUnit);
break;
} else if (diff == 0) {
deleteInternal(packagingUnit);
break;
} else {
demandedQty = demandedQty.subtract(packagingUnit.getQuantity());
deleteInternal(packagingUnit);
}
}
}
}
private void deleteInternal(PackagingUnit packagingUnit) {
var actualLocation = packagingUnit.getActualLocation();
var loadUnit = packagingUnit.getLoadUnit();
var currentProduct = packagingUnit.getProduct();
packagingUnit.kill(); // remove all references
var puClone = SerializationUtils.clone(packagingUnit); // deep copy the origin instance to pass also Product data over the wire
puClone.setProduct(new Product(currentProduct.getSku(), currentProduct.getBaseUnit()));
repository.save(packagingUnit);
if (actualLocation != null) {
eventPublisher.publishEvent(new PackagingUnitEvent(puClone, PackagingUnitEvent.TYPE.DELETED, actualLocation));
} else if (loadUnit != null) {
eventPublisher.publishEvent(new PackagingUnitEvent(puClone, PackagingUnitEvent.TYPE.DELETED, loadUnit));
} else {
throw new IllegalStateException("PackagingUnit must either be on a Location or in a LoadUnit");
}
}
}