Location.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.location;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.ameba.exception.BusinessRuntimeException;
import org.ameba.i18n.Translator;
import org.ameba.integration.jpa.ApplicationEntity;
import org.ameba.integration.jpa.BaseEntity;
import org.hibernate.envers.AuditOverride;
import org.hibernate.envers.Audited;
import org.openwms.wms.location.commands.LocationCommand;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.Objects;
import static org.openwms.wms.InventoryMessageCodes.LOCATION_AMOUNT_TU_EXCEEDED;
import static org.openwms.wms.api.TimeProvider.DATE_TIME_WITH_TIMEZONE;
/**
* A Location represents any location in a warehouse with the attributes relevant for the WMS Inventory Service.
*
* @author Heiko Scherrer
*/
@Audited
@AuditOverride(forClass = ApplicationEntity.class)
@AuditOverride(forClass = BaseEntity.class)
@Entity
@Table(name = "WMS_INV_LOCATION", uniqueConstraints = {
@UniqueConstraint(name = "UC_COORDINATE", columnNames = {"C_AREA", "C_AISLE", "C_X", "C_Y", "C_Z"}),
@UniqueConstraint(name = "UC_LOC_FOREIGN_PID", columnNames = {"C_FOREIGN_PID"})
})
public class Location extends ApplicationEntity implements Serializable {
/** The foreign persistent key of the Location. */
@Column(name = "C_FOREIGN_PID")
private String foreignPKey;
/** Unique natural key. */
@NotNull
@Embedded
private LocationPK locationId;
/** Unique identifier of a {@code LocationGroup}. */
@Column(name = "C_LOCATION_GROUP")
private String locationGroup;
/**
* Signals the incoming state of this Location.
* Locations which are blocked for incoming cannot pick up {@code TransportUnit}s.
* <ul>
* <li>{@literal true} : Location is ready to pick up {@code TransportUnit}s</li>
* <li>{@literal false}: Location is locked, and cannot pick up {@code TransportUnit}s</li>
* </ul>
*/
@Column(name = "C_INCOMING_ACTIVE")
private Boolean incomingActive;
/**
* Signals the outgoing state of this Location.
* Locations which are blocked for outgoing cannot release {@code TransportUnit}s.
* <ul>
* <li>{@literal true} : Location is enabled for outgoing {@code TransportUnit}s.</li>
* <li>{@literal false}: Location is locked, {@code TransportUnit}s can't leave this Location.</li>
* </ul>
*/
@Column(name = "C_OUTGOING_ACTIVE")
private Boolean outgoingActive;
/** Whether it is allowed to store different {@code Products} on this Location. */
@Column(name = "C_MIXED_PRODUCTS")
private Boolean mixedProducts;
/** Whether it is allowed to move {@code Products} without {@code TransportUnit} to this Location directly. */
@Column(name = "C_DIRECT_BOOKING_ALLOWED")
private Boolean directBookingAllowed = true;
/**
* The PLC is able to change the state of a Location. This property stores the last state, received from the PLC.
* <ul>
* <li>0 : No PLC error, everything okay</li>
* <li><0: Not defined</li>
* <li>>0: Some kind of defined error code</li>
* </ul>
*/
@Column(name = "C_PLC_STATE")
private int plcState;
/** ERP code of the Location. */
@Column(name = "C_ERP_CODE")
private String erpCode;
/** Description of the Location. */
@Size(max = 255)
@Column(name = "C_DESCRIPTION")
private String description;
/** Might be assigned to a particular zone in stock. */
@Column(name = "C_STOCK_ZONE")
private String stockZone;
/** The {@code Location} may be classified, like 'hazardous'. */
@Column(name = "C_CLASSIFICATION")
@Size(max = 255)
private String classification;
/** Sort order index used by Putaway strategies. */
@Column(name = "C_SORT")
private Integer sortOrder;
/** Maximum number of {@code TransportUnit}s allowed on this Location. */
@Column(name = "C_NO_MAX_TRANSPORT_UNITS")
private int noMaxTransportUnits = DEF_MAX_TU;
/** Default value of {@link #noMaxTransportUnits}. */
public static final int DEF_MAX_TU = 1;
/** When picking happened the last time on this Location. */
@Column(name = "C_LAST_PICK_DATE", columnDefinition = "timestamp(0)")
@DateTimeFormat(pattern = DATE_TIME_WITH_TIMEZONE)
private ZonedDateTime lastPickingDate;
/** When was this Location the last time under stock-taking. */
@Column(name = "C_LAST_INVENTORY_DATE", columnDefinition = "timestamp(0)")
@DateTimeFormat(pattern = DATE_TIME_WITH_TIMEZONE)
private ZonedDateTime lastInventoryDate;
/** Flag to determine if this Location is marked for deletion and cannot be used anymore. */
@Column(name = "C_MARKED_DELETION")
private boolean markForDeletion;
/*~------------ Constructors ------------*/
public Location(@NotNull LocationPK locationId) {
this.locationId = locationId;
}
/** Dear JPA ... */
protected Location() {
}
/*~------------ Methods ------------*/
/**
* Check whether this {@code Location} has free capacity to store {@code TransportUnit}s.
*
* @param currentNumberOfTransportUnits The current amount of TransportUnits currently placed in the Location
* @return {@literal true} If free capacity is left
*/
public boolean hasFreeSpaceAvailable(final int currentNumberOfTransportUnits) {
return this.getNoMaxTransportUnits() < 0 || currentNumberOfTransportUnits < this.getNoMaxTransportUnits();
}
/**
* Verify that additional capacity is available to store at least one {@code TransportUnit}.
*
* @param translator A Translator instance used to translate error messages
* @param currentNumberOfTransportUnits The current amount of TransportUnits currently placed in the Location
* @throws BusinessRuntimeException In case no space is left on Location
*/
public void verifyFreeSpaceAvailable(final Translator translator, final int currentNumberOfTransportUnits) {
if (!this.hasFreeSpaceAvailable(currentNumberOfTransportUnits)) {
throw new BusinessRuntimeException(translator, LOCATION_AMOUNT_TU_EXCEEDED,
new Serializable[]{this.erpCode, this.noMaxTransportUnits}, this.erpCode, this.noMaxTransportUnits);
}
}
/**
* Check whether the {@literal plcState} marks the {@code Location} as free of blocked.
*
* @return {@literal true} if free (not blocked)
*/
public boolean isPlcNotBlocked() {
return plcState == 0;
}
/**
* {@inheritDoc}
*
* Just the locationId.
*/
@Override
public String toString() {
return locationId.toString();
}
/**
* {@inheritDoc}
*
* All fields.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Location)) return false;
if (!super.equals(o)) return false;
Location location = (Location) o;
return plcState == location.plcState && noMaxTransportUnits == location.noMaxTransportUnits && Objects.equals(foreignPKey, location.foreignPKey) && Objects.equals(locationId, location.locationId) && Objects.equals(locationGroup, location.locationGroup) && Objects.equals(incomingActive, location.incomingActive) && Objects.equals(outgoingActive, location.outgoingActive) && Objects.equals(mixedProducts, location.mixedProducts) && Objects.equals(erpCode, location.erpCode) && Objects.equals(description, location.description) && Objects.equals(lastPickingDate, location.lastPickingDate) && Objects.equals(lastInventoryDate, location.lastInventoryDate);
}
/**
* {@inheritDoc}
*
* All fields.
*/
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), foreignPKey, locationId, locationGroup, incomingActive, outgoingActive, mixedProducts, plcState, erpCode, description, noMaxTransportUnits, lastPickingDate, lastInventoryDate);
}
/**
* Set this {@link Location} empty.
*
* @param eventPublisher An ApplicationEventPublisher instance that is used to publish change events
* @param plcState The PLC state to set, must be greater than 0
* @param incomingActive The state of incoming movements
* @param outgoingActive The state of outgoing movements
*/
public void setLocationEmpty(ApplicationEventPublisher eventPublisher, int plcState, boolean incomingActive, boolean outgoingActive) {
Assert.isTrue(plcState > 0, "The PLC state to block a Location must be greater than zero");
this.setPlcState(plcState);
this.setIncomingActive(incomingActive);
this.setOutgoingActive(outgoingActive);
eventPublisher.publishEvent(new LocationCommand(LocationCommand.Type.SET_LOCATION_EMPTY, this));
}
/*~------------ Accessors ------------*/
public String getForeignPKey() {
return foreignPKey;
}
public void setForeignPKey(String foreignPKey) {
this.foreignPKey = foreignPKey;
}
public LocationPK getLocationId() {
return locationId;
}
public boolean hasLocationId() {
return this.locationId != null;
}
public String getLocationGroup() {
return locationGroup;
}
public void setLocationGroup(String locationGroup) {
this.locationGroup = locationGroup;
}
public void setIncomingActive(boolean incomingActive) {
this.incomingActive = incomingActive;
}
public Boolean getIncomingActive() {
return incomingActive;
}
public Boolean getOutgoingActive() {
return outgoingActive;
}
public void setOutgoingActive(boolean outgoingActive) {
this.outgoingActive = outgoingActive;
}
public Boolean getMixedProducts() {
return mixedProducts;
}
public Boolean getDirectBookingAllowed() {
return directBookingAllowed;
}
public int getPlcState() {
return plcState;
}
public void setPlcState(int plcState) {
this.plcState = plcState;
}
public String getErpCode() {
return erpCode;
}
public boolean hasErpCode() {
return this.erpCode != null && !this.erpCode.isEmpty();
}
public void setErpCode(String erpCode) {
this.erpCode = erpCode;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getStockZone() {
return stockZone;
}
public String getClassification() {
return classification;
}
public Integer getSortOrder() {
return sortOrder;
}
public int getNoMaxTransportUnits() {
return noMaxTransportUnits;
}
public void setNoMaxTransportUnits(int noMaxTransportUnits) {
this.noMaxTransportUnits = noMaxTransportUnits;
}
public ZonedDateTime getLastPickingDate() {
return lastPickingDate;
}
public ZonedDateTime getLastInventoryDate() {
return lastInventoryDate;
}
public void setLastInventoryDate(ZonedDateTime lastInventoryDate) {
this.lastInventoryDate = lastInventoryDate;
}
public boolean isMarkForDeletion() {
return markForDeletion;
}
public void setMarkForDeletion(boolean markForDeletion) {
this.markForDeletion = markForDeletion;
}
}