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;
    }
}