LoadUnit.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;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.ameba.integration.jpa.ApplicationEntity;
import org.ameba.integration.jpa.BaseEntity;
import org.hibernate.envers.AuditOverride;
import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;
import org.openwms.wms.transport.TransportUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static jakarta.persistence.CascadeType.MERGE;
import static jakarta.persistence.CascadeType.PERSIST;
import static java.lang.String.format;
import static java.util.Arrays.asList;
/**
* A LoadUnit is used to divide a {@code TransportUnit} into multiple physical areas. It always requires a {@code TransportUnit} to be moved
* around, but it may contain {@code PackagingUnits} of arbitrary {@code Product} types or it may dedicated to one particular {@code Product}
* type only.
*
* @author Heiko Scherrer
*/
@Audited
@AuditOverride(forClass = ApplicationEntity.class)
@AuditOverride(forClass = BaseEntity.class)
@Entity
@Table(
name = "WMS_INV_LOAD_UNIT",
uniqueConstraints = {
@UniqueConstraint(name = "UC_LU_TUPK_POS", columnNames = {"C_TRANSPORT_UNIT_PK", "C_PHYSICAL_POS"}),
@UniqueConstraint(name = "UC_LU_LABEL", columnNames = {"C_LABEL"})
}
)
public class LoadUnit extends ApplicationEntity implements Serializable {
private static final Logger LOGGER = LoggerFactory.getLogger(LoadUnit.class);
/** The {@code TransportUnit} where this {@link LoadUnit} belongs to. */
@NotNull
@NotAudited
@ManyToOne
@JoinColumn(name = "C_TRANSPORT_UNIT_PK", foreignKey = @ForeignKey(name = "FK_LU_TU_PK"), nullable = false)
private TransportUnit transportUnit;
/** Where this {@link LoadUnit} is located on the {@code TransportUnit}. */
@NotEmpty
@Column(name = "C_PHYSICAL_POS", nullable = false)
private String physicalPosition;
/** An identifying label of the {@link LoadUnit}. */
@Column(name = "C_LABEL")
private String label;
/** The {@link LoadUnitType} the {@link LoadUnit} is of. */
@NotNull
@NotAudited
@ManyToOne
@JoinColumn(name = "C_LOAD_UNIT_TYPE", referencedColumnName = "C_TYPE", foreignKey = @ForeignKey(name = "FK_LUT_TYPE"), nullable = false)
private LoadUnitType type;
/** Locked for allocation. */
@Column(name = "C_LOCKED")
private boolean locked = false;
/** Whether it is allowed to store different {@code Products} in this LoadUnit. */
@Column(name = "C_MIXED_PRODUCTS")
private boolean mixedProducts = true;
/** The Product that is carried within the LoadUnit. */
@ManyToOne(fetch = FetchType.LAZY) //Define FetchType.LAZY for @ManyToOne association
@JoinColumn(name = "C_PRODUCT_PK", referencedColumnName = "C_PK", foreignKey = @ForeignKey(name = "FK_LU_PRODUCT_PK"))
private Product product;
/** The current dimension of the {@link LoadUnit}. */
@Embedded
private Dimension dimension;
/** All {@link PackagingUnit}s that belong to this {@link LoadUnit}. */
@OneToMany(mappedBy = "loadUnit", cascade = {PERSIST, MERGE})
private List<PackagingUnit> packagingUnits = new ArrayList<>();
/** Dear JPA ... */
protected LoadUnit() {
}
/**
* Create a new LoadUnit.
*
* @param transportUnit The {@code TransportUnit} where this LoadUnit stands on.
* @param physicalPosition The physical position within the {@code TransportUnit} where this LoadUnit stands on
* @param type The LoadUnitType to assign the LoadUnit to
*/
public LoadUnit(TransportUnit transportUnit, String physicalPosition, LoadUnitType type) {
this.transportUnit = transportUnit;
this.physicalPosition = physicalPosition;
this.type = type;
}
/**
* Get the transportUnit.
*
* @return the transportUnit.
*/
public TransportUnit getTransportUnit() {
return transportUnit;
}
/**
* Get the physicalPosition.
*
* @return the physicalPosition.
*/
public String getPhysicalPosition() {
return physicalPosition;
}
public String getLabel() {
return label;
}
/**
* Get the locked.
*
* @return the locked.
*/
public boolean isLocked() {
return locked;
}
/**
* Set the locked.
*
* @param locked The locked to set.
*/
public void setLocked(boolean locked) {
this.locked = locked;
}
public boolean isMixedProducts() {
return mixedProducts;
}
public void setMixedProducts(boolean mixedProducts) {
this.mixedProducts = mixedProducts;
}
/**
* Get the product.
*
* @return the product.
*/
public Product getProduct() {
return product;
}
/**
* Assign a Product to this LoadUnit.
*
* @param p The product to assign.
*/
public void assignProduct(Product p) {
if (p == null) {
throw new IllegalArgumentException("Calling assignProduct with null not allowed, therefore call unassignProduct");
}
if (!mixedProducts) {
Optional<PackagingUnit> first = getPackagingUnits().stream().findFirst();
if (first.isPresent() && !first.get().getProduct().getSku().equals(p.getSku())) {
throw new IllegalArgumentException(format(
"Not allowed to change the Product of this LoadUnit, because no mixed Products allowed, currently this LoadUnit contains [%s]",
first.get().getProduct().getSku()
));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Mixed products not allowed but no PackagingUnit assigned so far, assign this LoadUnit to Product [{}]", p.getSku());
}
}
this.product = p;
}
/**
* Unassign the product from this LoadUnit - set it to {@literal null}.
*/
public void unassignProduct() {
product = null;
}
public LoadUnitType getType() {
return type;
}
public Dimension getDimension() {
return dimension;
}
/**
* Get the packagingUnits.
*
* @return the packagingUnits.
*/
public List<PackagingUnit> getPackagingUnits() {
return initializeAndGet();
}
/**
* Check whether this {@link LoadUnit} has {@link PackagingUnit}s.
*
* @return {@literal true} if so
*/
public boolean hasPackagingUnits() {
return packagingUnits != null && !packagingUnits.isEmpty();
}
// This method is not an accessor and not woven by Hibernate in order to avoid performance issues!
private List<PackagingUnit> initializeAndGet() {
if (packagingUnits == null) {
packagingUnits = new ArrayList<>();
}
return packagingUnits;
}
/**
* Add one {@link PackagingUnit} to this LoadUnit.
*
* @param packagingUnit {@link PackagingUnit} to add
*/
public void addPackagingUnit(PackagingUnit packagingUnit) {
if (packagingUnit == null) {
return;
}
if (!mixedProducts && product != null && !product.getSku().equals(packagingUnit.getProduct().getSku())) {
throw new IllegalArgumentException(format(
"This LoadUnit is reserved for Product [%s] only and mixed Products aren't allowed. PackagingUnit to add is of Product type [%s]",
product.getSku(),
packagingUnit.getProduct().getSku()
));
}
packagingUnit.setLoadUnit(this);
initializeAndGet().add(packagingUnit);
}
/**
* Remove one or more {@link PackagingUnit}s from this LoadUnit.
*
* @param pUnits {@link PackagingUnit}s to remove
*/
public void removePackagingUnits(PackagingUnit... pUnits) {
if (pUnits != null && pUnits.length != 0) {
asList(pUnits).forEach(getPackagingUnits()::remove);
}
}
/**
* {@inheritDoc}
* <p/>
* Return a combination of the barcode and the physicalPosition.
*/
@Override
public String toString() {
if (transportUnit == null) {
return "n/a / " + physicalPosition;
}
return transportUnit.getTransportUnitBK() + " / " + physicalPosition;
}
/**
* {@inheritDoc}
* <p/>
* Use the TransportUnit and the Position only
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
LoadUnit loadUnit = (LoadUnit) o;
return Objects.equals(transportUnit, loadUnit.transportUnit) &&
Objects.equals(physicalPosition, loadUnit.physicalPosition);
}
/**
* {@inheritDoc}
* <p/>
* Use the TransportUnit and the Position only
*/
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), transportUnit, physicalPosition);
}
}