Document.java

package com.tradecloud.domain.document;

import com.tradecloud.common.base.HibernateUtils;
import com.tradecloud.common.base.PersistenceBase;
import com.tradecloud.domain.comment.Comment;
import com.tradecloud.domain.event.DocumentEvent;
import com.tradecloud.domain.event.DocumentEventType;
import com.tradecloud.domain.event.Event;
import com.tradecloud.domain.shipment.Shipment;
import com.tradecloud.domain.state.Stateful;
import com.tradecloud.domain.supplier.Creditor;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.log4j.Logger;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlID;
import java.util.*;

/**
 * Generic Document.
 * Note. This could also be called CostingDocument. Slightly confusing, as there is also a document in DMS.
 */
@MappedSuperclass
public abstract class Document extends PersistenceBase implements Comparable<Document>, Stateful<DocumentState, DocumentEvent> {

    protected final transient static Logger log = Logger.getLogger(Document.class);
    private static final long serialVersionUID = 1L;
    public final static Collection<DocumentState> NON_EDITABLE_STATES = Arrays.asList(DocumentState.SIGNED_OFF, DocumentState.SENT_TO_ERP,
            DocumentState.SETTLED, DocumentState.AWAITING_TREASURY_RATES, DocumentState.DOCUMENT_TOLERANCE_EXCEEDED, DocumentState.AWAITING_CREDIT,
            DocumentState.AWAITING_FINANCIAL_SUPERVISOR_SIGN_OFF, DocumentState.COMPLETE, DocumentState.PAYMENT_INITIATED, DocumentState.DELETED,
            DocumentState.PENDING_SIGN_OFF, DocumentState.OVER_PAID);
    /**
     * Business refer to this as "Document Date".
     *
     * @see https://jira.devstream.net/browse/BTM-1135
     */
    @NotNull(message = "Date created is required")
    @XmlAttribute
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    protected Date dateCreated;
    @NotNull(message = "Date received is required")
    @XmlAttribute
    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    protected Date dateReceived;
    @Temporal(TemporalType.DATE)
    @XmlAttribute
    protected Date actualSettlementDate;
    /**
     * The reference is not unique, users can choose the same one for a different document if they like.
     * There is client-config that can be enabled to show a warning when this happens.
     */
    @NotNull(message = "reference is required")
    @Size(max = 255, message = "invoice reference exceeded 255 character")
    @XmlID
    // XML id used for setting parent costsinvoice in child actualconsignment
    @XmlAttribute
    protected String reference;
    @XmlElementWrapper(name = "Comments")
    @XmlElement(name = "Comment")
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "costsinvoice_comment", joinColumns = {@JoinColumn(name = "costsinvoice_id")},
            inverseJoinColumns = {@JoinColumn(name = "comments_id")})
    protected List<Comment> documentsComments;
    /**
     * DocumentState INITIALISED after object initialisation. (This is an
     * internal state only)
     */
    @NotNull(message = "State is required")
    @XmlAttribute
    @Enumerated(EnumType.STRING)
    private DocumentState state = DocumentState.INITIALISED;
    /**
     * The document events, used to keep track of the history.
     */
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Fetch(value = FetchMode.SELECT)
    @XmlElementWrapper(name = "DocumentEvents")
    @XmlElement(name = "DocumentEvent")
    @OrderBy("createDateTime")
    private List<DocumentEvent> events = new LinkedList<>();

    private transient String orderReferences;

    public Document() {
        super();
    }

    public abstract DocumentType getDocumentType();

    public abstract Creditor getCreditor();

    @Override
    public Boolean getActive() {
        return state != DocumentState.DELETED;
    }

    public Date getDateCreated() {
        return dateCreated;
    }

    public void setDateCreated(Date dateCreated) {
        this.dateCreated = dateCreated;
    }

    public Date getDateReceived() {
        return dateReceived;
    }

    public void setDateReceived(Date dateReceived) {
        this.dateReceived = dateReceived;
    }

    public String getReference() {
        return reference;
    }

    public void setReference(String reference) {
        this.reference = reference;
    }

    public List<Comment> getDocumentsComments() {
        return documentsComments;
    }

    public void setDocumentsComments(List<Comment> documentsComments) {
        this.documentsComments = documentsComments;
    }

    @Override
    public DocumentState getState() {
        return state;
    }

    @Override
    public void setState(DocumentState state) {
        this.state = state;
    }

    @Override
    public List<DocumentEvent> getEvents() {
        return events;
    }

    public void setEvents(List<DocumentEvent> events) {
        this.events = events;
    }

    @Override
    public int compareTo(Document other) {
        if (getDocumentType().equals(other.getDocumentType())) {
            return getDateCreated().compareTo(other.getDateCreated());
        }
        return getDocumentType().compareTo(other.getDocumentType());
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append(dateCreated).append(dateReceived).append(reference).append(state).toString();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(dateCreated)
                .append(dateReceived)
                .append(reference)
                .toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        // Do not use class.equals. This can return false for proxy objects
        if (!HibernateUtils.proxyClassEquals(this, obj)) {
            return false;
        }
        Document other = (Document) obj;
        return new EqualsBuilder()
                .append(dateCreated, other.getDateCreated())
                .append(dateReceived, other.getDateReceived())
                .append(reference, other.getReference())
                .isEquals();
    }

    @Override
    public DocumentEvent getLastEvent() {
        return Event.getLastEvent(events);
    }

    @Deprecated
    public boolean editDisabled() {
        return inNonEditableState();
    }

    @Override
    public boolean inNonEditableState() {
        return NON_EDITABLE_STATES.contains(state);
    }

    public Date getActualSettlementDate() {
        return actualSettlementDate;
    }

    public void setActualSettlementDate(Date actualSettlementDate) {
        this.actualSettlementDate = actualSettlementDate;
    }

    public abstract Shipment getShipment();

    public DocumentEvent getLastEvent(DocumentEventType documentEventType) {
        List<DocumentEvent> copyEvents = new ArrayList<>(events);
        Collections.reverse(copyEvents);
        return Event.getFirstEvent(copyEvents, documentEventType);
    }

    public boolean isCostsInvoice() {
        return false;
    }

    public boolean isSettled() {
        return state == DocumentState.SETTLED;
    }

    public boolean isPostSignOffState() {
        return this.state == DocumentState.SIGNED_OFF || state == DocumentState.SETTLED || state == DocumentState.OVER_PAID;
    }

    public DocumentEvent getDocumentEventSignOffEvent() {
        // 1) Prefer the latest SIGNED_OFF if it exists
        DocumentEvent signedOff = getLastEvent(DocumentEventType.SIGNED_OFF);
        if (signedOff != null) {
            return signedOff;
        }

        // 2) Look at the truly last event, but guard for null
        DocumentEvent last = getLastEvent();
        if (last == null) {
            return null;
        }

        // 3) Only if the last event is NOT PENDING_SIGN_OFF,
        //    return the latest PENDING_SIGN_OFF (may still be null)
        if (last.getEventType() != DocumentEventType.PENDING_SIGN_OFF) {
            return getLastEvent(DocumentEventType.PENDING_SIGN_OFF);
        }

        return null;
    }

    public String getOrderReferences() {
        return orderReferences;
    }

    public void setOrderReferences(String orderReferences) {
        this.orderReferences = orderReferences;
    }
}