PackagingUnitCreatorImpl.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.Valid;
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.exception.BadRequestException;
import org.ameba.exception.NotFoundException;
import org.ameba.i18n.Translator;
import org.openwms.wms.api.TimeProvider;
import org.openwms.wms.inventory.LoadUnit;
import org.openwms.wms.inventory.LoadUnitService;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.PackagingUnitCreator;
import org.openwms.wms.inventory.ProductService;
import org.openwms.wms.inventory.api.ValidationGroups;
import org.openwms.wms.inventory.events.PackagingUnitEvent;
import org.openwms.wms.location.LocationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.validation.annotation.Validated;
import java.io.Serializable;
import java.util.List;
import java.util.ServiceLoader;
import static org.openwms.wms.InventoryMessageCodes.LOAD_UNIT_NO_TYPE_GIVEN;
import static org.openwms.wms.InventoryMessageCodes.PU_NO_ACTUAL_LOCATION;
import static org.openwms.wms.InventoryMessageCodes.PU_NO_PRODUCT_WITH_UNIT;
/**
* A PackagingUnitCreatorImpl is a Spring managed transactional bean that is responsible for creating {@link PackagingUnit}s.
*
* @author Heiko Scherrer
*/
@Validated
@TxService
class PackagingUnitCreatorImpl implements PackagingUnitCreator {
private static final Logger LOGGER = LoggerFactory.getLogger(PackagingUnitCreatorImpl.class);
private final TimeProvider timeProvider = ServiceLoader.load(TimeProvider.class).iterator().next();
private final ApplicationEventPublisher eventPublisher;
private final Translator translator;
private final PackagingUnitRepository repository;
private final LoadUnitService loadUnitService;
private final LocationService locationService;
private final ProductService productService;
PackagingUnitCreatorImpl(ApplicationEventPublisher eventPublisher, Translator translator, PackagingUnitRepository repository,
LoadUnitService loadUnitService, LocationService locationService, ProductService productService) {
this.eventPublisher = eventPublisher;
this.translator = translator;
this.repository = repository;
this.loadUnitService = loadUnitService;
this.locationService = locationService;
this.productService = productService;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public PackagingUnit createInContainer(@NotBlank String transportUnitBK, @NotBlank String luPos,
@NotNull PackagingUnit packagingUnit, String loadUnitType) {
var loadUnit = getOrCreateLoadUnit(transportUnitBK, luPos, loadUnitType);
return createInternal(loadUnit, packagingUnit);
}
private PackagingUnit createInternal(LoadUnit loadUnit, PackagingUnit packagingUnit) {
packagingUnit.setProduct(productService.findBySKUorThrow(packagingUnit.getProduct().getSku()));
packagingUnit.completeBeforeCreation(timeProvider.nowAsDate());
loadUnit.addPackagingUnit(packagingUnit);
loadUnitService.save(loadUnit);
// Saving the PU happens cascaded
eventPublisher.publishEvent(new PackagingUnitEvent(packagingUnit, PackagingUnitEvent.TYPE.CREATED));
return packagingUnit;
}
private LoadUnit getOrCreateLoadUnit(String transportUnitBK, String luPos, String loadUnitType) {
var lu = loadUnitService.findOptional(transportUnitBK, luPos);
LoadUnit loadUnit;
if (lu.isPresent()) {
loadUnit = lu.get();
LOGGER.debug("LoadUnit exists: [{}]", loadUnit);
} else {
if (loadUnitType == null) {
throw BadRequestException.createFromKey(translator, LOAD_UNIT_NO_TYPE_GIVEN, new Serializable[]{luPos, transportUnitBK}, luPos, transportUnitBK);
}
loadUnit = loadUnitService.create(transportUnitBK, luPos, loadUnitType);
LOGGER.debug("LoadUnit [{}] of type [{}] has been created on TransportUnit [{}]", luPos, loadUnitType, transportUnitBK);
}
return loadUnit;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
public void createMultipleInContainer(@NotBlank String transportUnitBK, @NotBlank String luPos,
@NotEmpty List<PackagingUnit> packagingUnits, String loadUnitType) {
var loadUnit = getOrCreateLoadUnit(transportUnitBK, luPos, loadUnitType);
packagingUnits.forEach(pu -> createInternal(loadUnit, pu));
loadUnitService.save(loadUnit);
}
/**
* {@inheritDoc}
*/
@Override
@Measured
@Validated(ValidationGroups.CreatePackagingUnit.class)
public PackagingUnit create(@NotNull @Valid PackagingUnit packagingUnit) {
return createInternal(packagingUnit);
}
private PackagingUnit createInternal(PackagingUnit packagingUnit) {
final var sku = packagingUnit.getProduct().getSku();
packagingUnit.setProduct(productService.findBySKUorThrow(sku));
if (!packagingUnit.hasLoadUnit()) {
if (!packagingUnit.hasActualLocation()) {
throw BadRequestException.createFromKey(translator, PU_NO_ACTUAL_LOCATION, new Serializable[]{});
}
var location = packagingUnit.getActualLocation().getErpCode() != null
? locationService.findByErpCode(packagingUnit.getActualLocation().getErpCode())
: locationService.findByBK(packagingUnit.getActualLocation().getLocationId());
packagingUnit.setActualLocation(location);
}
if (packagingUnit.hasUomRelation()) {
final var puUnit = packagingUnit.getUomRelation().getUnit();
// Does the combination of Product and UomRelation exist and is allowed?
var p = productService
.findBySKUandUOM(sku, puUnit.asString())
.orElseThrow(() -> new NotFoundException(
translator,
PU_NO_PRODUCT_WITH_UNIT,
sku,
puUnit.asString())
);
var unit = p.getUnits().stream().filter(u -> u.getUnit().getUnitType().equals(puUnit.getUnitType())).findFirst();
if (unit.isEmpty()) {
throw new NotFoundException(translator, PU_NO_PRODUCT_WITH_UNIT, sku, puUnit.asString());
}
packagingUnit.setUomRelation(unit.get());
}
packagingUnit.completeBeforeCreation(timeProvider.nowAsDate());
packagingUnit = repository.save(packagingUnit);
eventPublisher.publishEvent(new PackagingUnitEvent(packagingUnit, PackagingUnitEvent.TYPE.CREATED));
return packagingUnit;
}
/**
* {@inheritDoc}
*/
@Override
@Measured
@Validated(ValidationGroups.CreatePackagingUnit.class)
public List<PackagingUnit> create(@NotEmpty @Valid List<PackagingUnit> packagingUnits) {
return packagingUnits.stream().map(this::createInternal).toList();
}
}