Order.java

package com.tradecloud.domain.model.ordermanagement;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.tradecloud.common.base.HibernateUtils;
import com.tradecloud.common.base.PersistenceBase;
import com.tradecloud.domain.base.utils.DateUtils;
import com.tradecloud.domain.base.utils.MathUtils;
import com.tradecloud.domain.base.utils.ObjectUtil;
import com.tradecloud.domain.comment.AddedComment;
import com.tradecloud.domain.comment.AddedCommentI;
import com.tradecloud.domain.comment.CommentType;
import com.tradecloud.domain.comment.Commentable;
import com.tradecloud.domain.common.Currency;
import com.tradecloud.domain.common.Incoterm;
import com.tradecloud.domain.configuration.SpecialRequirementConfig;
import com.tradecloud.domain.container.Container;
import com.tradecloud.domain.costing.clean.CostingVisitor;
import com.tradecloud.domain.costing.clean.EstimateCostSummary;
import com.tradecloud.domain.dms.DocumentGroupState;
import com.tradecloud.domain.dms.DocumentManagementHardCoding;
import com.tradecloud.domain.document.invoice.PlannedSettlementHelper;
import com.tradecloud.domain.document.invoice.UnitPricePerItem;
import com.tradecloud.domain.event.ActivityLog;
import com.tradecloud.domain.event.Event;
import com.tradecloud.domain.event.OrderEventType;
import com.tradecloud.domain.event.OrdersEvent;
import com.tradecloud.domain.item.AdditionalNotes;
import com.tradecloud.domain.item.ItemType;
import com.tradecloud.domain.item.LineItem;
import com.tradecloud.domain.model.DMSLinked;
import com.tradecloud.domain.model.Original;
import com.tradecloud.domain.model.goodsreceivedreceipt.GoodsReceivedReceipt;
import com.tradecloud.domain.model.organisationalunit.OrganisationalUnit;
import com.tradecloud.domain.model.payment.PaymentMethod;
import com.tradecloud.domain.model.payment.PaymentTerm;
import com.tradecloud.domain.party.Employee;
import com.tradecloud.domain.party.ServiceProvider;
import com.tradecloud.domain.party.base.Contact;
import com.tradecloud.domain.payment.Payable;
import com.tradecloud.domain.place.PlaceOfDischarge;
import com.tradecloud.domain.settlement.PlannedSettlement;
import com.tradecloud.domain.settlement.Settleable;
import com.tradecloud.domain.shipment.ShippingInformation;
import com.tradecloud.domain.shipment.SubShipment;
import com.tradecloud.domain.state.Stateful;
import com.tradecloud.domain.supplier.SupplierCommon;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.*;

import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;

import static com.tradecloud.domain.model.ordermanagement.OrderState.*;

//import org.hibernate.annotations.Where;

/**
 * Base class for orders in the system.
 * https://connect.devstream.net/display/Dev/Order+Fields.
 *
 * @see PurchaseOrder
 * @see SalesOrder
 */
@Entity
@Table(name = "orders", uniqueConstraints = {@UniqueConstraint(columnNames = {"number"}), @UniqueConstraint(columnNames = {"orderReference"})})
@FilterDef(name = "excludeCopies", defaultCondition = "copy is false")
@Inheritance(strategy = InheritanceType.JOINED)
@Access(AccessType.FIELD)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "Order")
@XmlSeeAlso({PurchaseOrder.class, SalesOrder.class})
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@NamedQueries({
        @NamedQuery(name = "order.findByNumber", query = "SELECT o FROM Order o WHERE o.number = :number"),
        @NamedQuery(name = "order.findByShippingReference",
                query = "SELECT o FROM Order o WHERE o.shippingInformation.shippingReference = :shippingReference"),
        @NamedQuery(name = "order.findAllArchivedByReference",
                query = "FROM Order o WHERE o.orderReference = :reference AND o.state in ('ARCHIVED') ORDER BY o.created"),
        @NamedQuery(name = "order.findByReference", query = "SELECT o FROM Order o WHERE o.orderReference = :orderReference " +
                "AND o.state NOT IN ('ARCHIVED')"),
        @NamedQuery(name = "order.findByQuote", query = "SELECT o FROM Order o WHERE o.quoteReference = :quoteReference " +
                "AND o.state NOT IN ('ARCHIVED')"),
        @NamedQuery(name = "order.findByReferenceNotDeleted",
                query = "SELECT o FROM Order o where o.orderReference = :orderReference and o.state not in ('DELETED', 'ARCHIVED')"),
        @NamedQuery(name = "order.findByNumberAndReference",
                query = "SELECT o FROM Order o WHERE o.number = :orderNumber AND o.orderReference = :orderReference"),
        @NamedQuery(name = "findAllUnlinkedOrders", query = "SELECT o FROM Order o WHERE o.consignment IS NULL and o.elc='f'"),
        // moved to subclass
        // @NamedQuery(name = "order.findAllUnlinkedWithItems",
        // query = "from Order o where o.state not in ('DELETED') and o.consignment is null and o.lineItems.size > 0"),
        @NamedQuery(name = "order.findAllByState", query = "from Order o where o.state = :state and elc='f'"),
        @NamedQuery(name = "order.findById",
                query = "from Order ord left join fetch ord.lineItems as lineItems where ord.id=:id" +
                        " order by lineItems.created"),
        @NamedQuery(name = "order.findAllByShipment", query = "from Order o where o.consignment.shipment.id = :id"),
        @NamedQuery(name = "order.findByIdWithEventsAndPlannedSettlements",
                query = "from Order ord left join fetch ord.events left join fetch ord.plannedSettlements where ord.id = :id")})
public abstract class Order extends PersistenceBase implements Commentable<AddedComment>, Original, Stateful<OrderState, OrdersEvent>,
        Comparable<Order>, Payable, Settleable, DMSLinked {

    public enum TradeAgreementOption {
        COUNTRY_OF_ORIGIN, GENERAL
    }

    /**
     * The name of the database sequence used for generating part of the order
     * number.
     *
     * @see #number
     */
    public static final String NUMBER_SEQUENCE = "ordernumber_sequence";
    public final static Collection<OrderState> NON_EDITABLE_STATES = Arrays.asList(OrderState.SIGNED_OFF, OrderState.AWAITING_LSP_SIGNOFF,
            OrderState.STOCK_RECEIVED, OrderState.BOOKED_IN, OrderState.FREIGHT_RECEIVED, OrderState.DELETED, OrderState.FINALISED,
            SHIPMENT_CREATED, IN_EXECUTION, CARGO_READY, AWAITING_BOOKING, AWAITING_COLLECTION, SHIPMENT_CREATED, ARCHIVED);
    protected final static Collection<OrderState> SALE_NON_EDITABLE_STATES = Arrays.asList(OrderState.SIGNED_OFF, OrderState.AWAITING_LSP_SIGNOFF,
            OrderState.STOCK_RECEIVED, OrderState.BOOKED_IN, OrderState.FREIGHT_RECEIVED, OrderState.DELETED, OrderState.FINALISED,
            STOCK_PARTIALLY_RECEIVED);
    private static final long serialVersionUID = 1L;
    public final static Collection<OrderState> SIGNED_OFF_STATES = Arrays.asList(OrderState.SIGNED_OFF, OrderState.AWAITING_LSP_SIGNOFF,
            OrderState.BOOKED_IN, OrderState.FREIGHT_RECEIVED, SUPPLIER_BOOKING_REQUESTED, CARGO_READY, SHIPMENT_CREATED);
    public final static Collection<OrderState> PAYABLE_STATES = Arrays.asList(OrderState.SIGNED_OFF, OrderState.AWAITING_LSP_SIGNOFF,
            OrderState.BOOKED_IN, OrderState.FREIGHT_RECEIVED, FINALISED, STOCK_RECEIVED, TOLERANCE_EXCEEDED, SHIPMENT_CREATED, CANCEL_BOOKING,
            STOCK_PARTIALLY_RECEIVED);
    @Transient
    protected String reason;
    protected boolean confirmed;
    /**
     * Unique system generated number. The format for this is configured in the
     * client setup.
     *
     * @see #NUMBER_SEQUENCE
     */
    @XmlID
    @XmlAttribute(required = true)
    @NaturalId
    @NotNull(message = "Number is required")
    @Size(max = 255)
    protected String number;
    /**
     * The client's ERP order number.
     * XML id used for setting parent order in child lineitem
     */
    @XmlID
    @XmlAttribute(required = true)
    @NotNull(message = "Reference is required")
    @Size(max = 255)
    protected String orderReference;
    @XmlTransient
    @Size(max = 255)
    @JsonIgnore
    protected String oldOrderReference;
    /**
     * The organisational unit for which the buyer or seller places the order.
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @XmlElement(name = "OrganisationalUnit", required = true)
    @NotNull(message = "Organisational Unit is required")
    protected OrganisationalUnit organisationalUnit;
    /**
     * The currency of the order. This should default to the the supplier's
     * currency but if no default exists there then the currency configured in
     * the client setup will be used.
     */
    @ManyToOne
    @XmlElement(name = "Currency", required = true)
    @NotNull(message = "Currency is required")
    protected Currency currency;
    /**
     * The payment method of the order for example LC, DRAFT, CASH etc. This
     * should default to the the supplier's payment method. If there is no
     * payment method provided for the supplier then the payment method
     * configured in the client setup will be used.
     */
    @ManyToOne
    @XmlElement(name = "PaymentMethod", required = true)
    @NotNull(message = "Payment Method is required")
    protected PaymentMethod paymentMethod;
    /**
     * The payment term on the order for example 30 day, 90 day etc. This should
     * default to the the supplier's payment term. If there is no payment term
     * provided for the supplier then the payment term configured in the client
     * setup will be used.
     */
    @ManyToOne
    @XmlElement(name = "PaymentTerm", required = true)
    @NotNull(message = "Payment Term is required")
    protected PaymentTerm paymentTerm;
    /**
     * Set of line Items contained on the order. There is no LineItem service,
     * they are managed only as part of the order
     */
    @XmlElementWrapper(name = "LineItems")
    @XmlElement(name = "LineItem")
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "order", orphanRemoval = true, fetch = FetchType.LAZY)
    @Fetch(value = FetchMode.SELECT)
    @OrderBy("position,addedToOrderDate,code")
    @JsonManagedReference
    protected Set<LineItem> lineItems = new LinkedHashSet<LineItem>();
    /**
     * Set of planned settlements contained on the order. There is no Planned Settlement service,
     * they are managed only as part of the order
     */
    @XmlElementWrapper(name = "PlannedSettlements")
    @XmlElement(name = "PlannedSettlement")
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true,mappedBy = "order")
        @OrderBy("settlementDate")
    protected Set<PlannedSettlement> plannedSettlements = new HashSet<PlannedSettlement>();
    /**
     * The consignment that this order is associated with.
     */
    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "consignment_id")
    @XmlTransient
    @JsonIgnore
    protected Consignment consignment;
    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "subShipment_id")
    @XmlTransient
    @JsonIgnore
    protected SubShipment subShipment;
    /**
     * Various dates relating to the order.
     */
    @Embedded
    @XmlElement(name = "OrderDates")
    protected OrderDates orderDates = new OrderDates();
    /**
     * All shipping information for the order.
     */
    @XmlElement(name = "ShippingInformation")
    @OneToOne(cascade = CascadeType.ALL)
    protected ShippingInformation shippingInformation = new ShippingInformation();
    /**
     * The total value of all the items on the order, it should match the value
     * specified on the pro-forma invoice.
     */
    @XmlAttribute
    protected BigDecimal totalInvoiceValue;
    /**
     * Indicates that the LC can be drawn for X% more than the face value of the
     * LC. Should not be less than 0 and more than 10.
     *
     * This should default to the the supplier's lcToleranceAbove value. If
     * there is no lcToleranceAbove provided for the supplier then the
     * lcToleranceAbove configured in the client setup will be used.
     */
    @XmlAttribute
    protected BigDecimal lcToleranceAbove;
    /**
     * Indicates that the LC can be drawn for X% less than the face value of the
     * LC. Should not be less than 0 and more than 10.
     *
     * This should default to the the supplier's lcToleranceBelow value. If
     * there is no lcToleranceBelow provided for the supplier then the
     * lcToleranceBelow configured in the client setup will be used.
     */
    @XmlAttribute
    protected BigDecimal lcToleranceBelow;
    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "orders_freetextcomments", joinColumns = {@JoinColumn(name = "order_id", unique = false)})
    @Column(name = "reason", unique = true)
    @ForeignKey(name = "fk_orders")
    @XmlElementWrapper(name = "FreeTextComments")
    @XmlElement(name = "FreeTextComment")
    @Fetch(value = FetchMode.SELECT)
    protected List<AddedComment> comments = new ArrayList<AddedComment>();
    /**
     * The reference displayed on the proforma invoice received from the
     * supplier.
     */
    @XmlAttribute
    @Size(max = 255)
    protected String proFormaReference;
    /**
     * Indicates if the unit price on an invoice includes or excludes additional
     * costs (e.g.inland freight). Available options are: Factory unit price and
     * Total unit price. This should default to the the supplier's unit price
     * per item. If there is no unit price per item provided for the supplier
     * then the unit price per item configured in the client setup will be used.
     */
    @Enumerated(value = EnumType.STRING)
    @XmlAttribute
    protected UnitPricePerItem unitPricePerItem;
    /**
     * Fields about the letter of credit the order needs.
     */
    @XmlElement(name = "OrderLetterOfCredit")
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @ForeignKey(name = "fk_orderletterofcredit")
    protected OrderLetterOfCredit orderLetterOfCredit;
    /**
     * The booking reference received from the LSP.
     */
    @XmlAttribute
    @Size(max = 255)
    protected String lspBookingReference;
    /**
     * The name of the vessel that the goods are loaded on.
     */
    @XmlAttribute
    @Size(max = 255)
    protected String vesselName;

    @Enumerated(EnumType.STRING)
    @XmlElement(name = "ShipmentType")
    protected ShipmentType shipmentType;
    /**
     * OrderState INITIALISED after object initialisation. (This is an internal
     * state only)
     */
    @NotNull(message = "State is required")
    // @XmlAttribute
    @Enumerated(EnumType.STRING)
    protected OrderState state = OrderState.UNFINALISED;

    @NotNull(message = "Business State is required")
    @Enumerated(EnumType.STRING)
    protected BusinessState businessState = BusinessState.ON_ORDER;
    /**
     * The purchase order events, used to keep track of the history.
     */
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinTable(name = "orders_ordersevent", joinColumns = {@JoinColumn(name = "orders_id")},
            inverseJoinColumns = {@JoinColumn(name = "events_id")})
    @Fetch(value = FetchMode.SELECT)
    @XmlElementWrapper(name = "OrderEvents")
    @XmlElement(name = "OrderEvent")
    @OrderBy("createDateTime")
    protected List<OrdersEvent> events = new LinkedList<OrdersEvent>();
    @XmlAttribute
    @Column
    protected Date originalDocumentsReceivedDate;
    /**
     * Indicates if order was created via integration interface.
     */
    @XmlAttribute
    protected boolean integrated;
    protected boolean uploaded = false;
    @XmlAttribute
    @Size(max = 255)
    protected String deletedReason;
    @XmlAttribute
    @Temporal(TemporalType.TIMESTAMP)
    protected Date addedToConsignmentDate;
    /**
     * Indicates whether a deal in TTM should be created for this order.
     */
    @XmlAttribute
    protected boolean exposureRequired = true;
    @Transient
    @XmlTransient
    EstimateCostSummary costingSummary;
    private boolean elc;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinFormula("(SELECT MAX(orders_activitylogs.activitylogs_id) " +
            "FROM orders_activitylogs " +
            "WHERE orders_activitylogs.orders_id = id)")
    private ActivityLog lastActivity;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @Fetch(value = FetchMode.SELECT)
    @XmlElementWrapper(name = "ICPOrderEvents")
    @XmlElement(name = "ICPOrderEvent")
    @OrderBy("createDateTime")
    @JoinTable(name = "orders_activitylogs", joinColumns = {
            @JoinColumn(name = "orders_id", unique = false)},
            inverseJoinColumns = {
                    @JoinColumn(name = "activitylogs_id", unique = false)})
    protected List<ActivityLog> activityLogs = new LinkedList<>();

    //    @OneToMany(cascade = {CascadeType.ALL}, orphanRemoval = true, fetch = FetchType.LAZY)
//    @JoinTable(name = "orders_tradefinance", joinColumns = {@JoinColumn(name = "orders_id")},
//            inverseJoinColumns = {@JoinColumn(name = "tradefinance_id")})
//    @JsonIgnore
//    private List<TradeFinance> tradeFinance;
    @ManyToOne(cascade = {CascadeType.MERGE})
    private SpecialRequirementConfig specialRequirementConfig;
    private String specialRequirementOtherDescription;
    @OneToMany(cascade = {CascadeType.ALL}, mappedBy = "order", orphanRemoval = true, fetch = FetchType.LAZY)
    private Set<GoodsReceivedReceipt> goodsReceivedReceipts = new HashSet<>();
    @ManyToOne(fetch = FetchType.LAZY)
    @XmlElement(name = "supplierContact")
    private Contact supplierContact;
    @Transient
    private boolean totalsInUse;
    private Date stateDate;

    @Enumerated(EnumType.STRING)
    TradeAgreementOption defaultTradeAgreementOption = TradeAgreementOption.COUNTRY_OF_ORIGIN;
    @Enumerated(EnumType.STRING)
    private ItemType defaultItemType = ItemType.GENERAL;

    /**
     * Indicates whether an order had a booking date before.
     */
    @XmlAttribute
    protected boolean previouslyBooked = true;

    private Long bulkOrderId;
    private boolean bulkOrder;

    @Basic(fetch = FetchType.LAZY)
    @Formula("""
    (
        SELECT SUM(i.unitquantity)
        FROM lineitem i
        WHERE i.order_id = id
    )
""")
    private BigDecimal orderQuantity;

    @Basic(fetch = FetchType.LAZY)
    @Formula("""
    (
        SELECT 
            CASE 
                WHEN SUM(i.unitquantity) > 0 
                THEN SUM(i.sellPriceExclusiveAmount * i.unitquantity) / SUM(i.unitquantity)
                ELSE 0
            END
        FROM lineitem i
        WHERE i.order_id = id
    )
""")
    private BigDecimal weightedSellingPriceExclusive;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    @Fetch(value = FetchMode.SELECT)
    @JoinTable(name = "orders_AdditionalNotes", joinColumns = @JoinColumn(name = "orders_id"),
            inverseJoinColumns = @JoinColumn(name = "notes_id"))
    private Set<AdditionalNotes> additionalNotes;

    private String quoteReference;
    private boolean earlyOrder = false;

    @ManyToOne
    private ServiceProvider plannedCargoCarrier;

    @ManyToOne
    @XmlElement(name = "PlannedPlaceOfDischarge")
    private PlaceOfDischarge plannedPlaceOfDischarge;

    @ManyToOne(fetch = FetchType.LAZY)
    private OrganisationalUnit company;

    public enum Type {
        FULL, ELC
    }

    public Order() {
    }

    public Date getStateDate() {
        return stateDate;
    }

    public void setStateDate(Date stateDate) {
        this.stateDate = stateDate;
    }

    public List<ActivityLog> getActivityLogs() {
        return activityLogs;
    }

    public void setActivityLogs(List<ActivityLog> activityLogs) {
        this.activityLogs = activityLogs;
    }

    public ActivityLog getLastActivity() {
        if (this.lastActivity == null) {
            return Event.getLastEvent(activityLogs);
        }
        return this.lastActivity;
    }

    // TODO - This constructor has way too many params.Should use a builder.
    public Order(String number, String orderReference, OrganisationalUnit organisationalUnit, Set<LineItem> lineItems, OrderDates orderDates,
                 ShippingInformation shippingInformation, Currency currency, PaymentMethod paymentMethod, PaymentTerm paymentTerm,
                 Incoterm incoterm) {
        super();
        this.number = number;
        this.orderReference = orderReference;
        this.organisationalUnit = organisationalUnit;
        this.lineItems = lineItems;
        this.orderDates = orderDates;
        this.shippingInformation = shippingInformation;
        this.currency = currency;
        this.paymentMethod = paymentMethod;
        this.paymentTerm = paymentTerm;
        getShippingInformation().setIncoterm(incoterm);
    }

    public String getCommentsAsString() {
        return AddedCommentI.commentsAsString(getComments());
    }

    public boolean isLinkedToShipment() {

        return this.getConsignment() != null && this.getConsignment().getShipment() != null;

    }

    public abstract OrderType getType();

    @Override
    public Boolean getActive() {
        return !ObjectUtil.containsAll(this.state.name(), OrderState.DELETED.name(), ARCHIVED.name());
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getOrderReference() {
        return orderReference;
    }

    public void setOrderReference(String orderReference) {
        // TODO. Remove this check. Why is it here?
        if (oldOrderReference == null) {
            oldOrderReference = this.orderReference;
        }
        this.orderReference = orderReference;
    }

    public OrganisationalUnit getOrganisationalUnit() {
        return organisationalUnit;
    }

    public void setOrganisationalUnit(OrganisationalUnit organisationalUnit) {
        this.organisationalUnit = organisationalUnit;
    }

    public OrderDates getOrderDates() {
        return orderDates;
    }

    public void setOrderDates(OrderDates orderDates) {
        this.orderDates = orderDates;
    }

    public ShippingInformation getShippingInformation() {
        return shippingInformation;
    }

    public void setShippingInformation(ShippingInformation shippingInformation) {
        this.shippingInformation = shippingInformation;
    }

    @Override
    public Currency getCurrency() {
        return currency;
    }

    public void setCurrency(Currency currency) {
        this.currency = currency;
    }

    public BigDecimal getLcToleranceAbove() {
        return lcToleranceAbove;
    }

    public void setLcToleranceAbove(BigDecimal lcToleranceAbove) {
        this.lcToleranceAbove = lcToleranceAbove;
    }

    public BigDecimal getLcToleranceBelow() {
        return lcToleranceBelow;
    }

    public void setLcToleranceBelow(BigDecimal lcToleranceBelow) {
        this.lcToleranceBelow = lcToleranceBelow;
    }

    public Consignment getConsignment() {
        return consignment;
    }

    public void setConsignment(Consignment consignment) {
        this.consignment = consignment;
    }

    public Set<LineItem> getLineItems() {
        return lineItems;
    }

    public void setLineItems(Set<LineItem> lineItems) {
        this.lineItems = lineItems;
    }

    /**
     * @deprecated Use getActiveLineItems.
     * Needed for a JSF page where we reference the line items directly on an
     * order.
     */
    @Deprecated
    public List<LineItem> getLineItemsList() {
        Collections.sort(new ArrayList<LineItem>(lineItems));
        return new ArrayList<LineItem>(lineItems);
    }

    public ServiceProvider getPlannedCargoCarrier() {
        return plannedCargoCarrier;
    }

    public void setPlannedCargoCarrier(ServiceProvider plannedCargoCarrier) {
        this.plannedCargoCarrier = plannedCargoCarrier;
    }

    public PlaceOfDischarge getPlannedPlaceOfDischarge() {
        return plannedPlaceOfDischarge;
    }

    public void setPlannedPlaceOfDischarge(PlaceOfDischarge plannedPlaceOfDischarge) {
        this.plannedPlaceOfDischarge = plannedPlaceOfDischarge;
    }

    public List<LineItem> getActiveLineItems() {
        return PersistenceBase.getActiveList(lineItems);
    }

    public void addLineItem(LineItem lineItem) {
        lineItem.setOrder(this);
        lineItems.add(lineItem);
        // Slight hack. Need to preserve the date in the case of a copy.
        // Then then date will be already set.
        // Another option would be to have another addLineItem(LineItem, Date)
        if (lineItem.getAddedToOrderDate() == null) {
            lineItem.setAddedToOrderDate(new Date());
        }
        calculateTotalInvoiceValue();
    }

    public void addLineItems(Set<LineItem> lineItems) {
        int i = 1;
        for (LineItem lineItem : lineItems) {
            lineItem.setOrder(this);
            this.lineItems.add(lineItem);
            if (lineItem.getAddedToOrderDate() == null) {
                //new Date(), does not guarantee unique date hence we add nano secs
                lineItem.setAddedToOrderDate(DateUtils.addNanoSEcs(new Date(), i++ * 1000000));
            }
        }
        calculateTotalInvoiceValue();
    }

    public void removeLineItem(LineItem lineItem) {
        lineItems.remove(lineItem);
        lineItem.setOrder(null);
        lineItem.setAddedToOrderDate(null);
        calculateTotalInvoiceValue();
    }

    public void removePlannedSettlement(PlannedSettlement plannedSettlement) {
        plannedSettlements.remove(plannedSettlement);
    }

    public void clearPlannedSettlements() {
        this.plannedSettlements.clear();
    }

    public void addPlannedSettlement(PlannedSettlement plannedSettlement) {
        plannedSettlements.add(plannedSettlement);
    }

    // TODO. Move this out. Make it a query. This is SLOW. It is killing reports.
    @Deprecated
    public BigDecimal getTotalLineItemWeight() {
        BigDecimal totalLineItemWeight = BigDecimal.ZERO;
        BigDecimal cargoWeight = BigDecimal.ZERO;
        if (consignment != null && consignment.getContainers() != null) {
            for (Container container : consignment.getContainers()) {
                if (container.getWeightKG() != null)
                    cargoWeight = cargoWeight.add(container.getWeightKG());
            }
        }
        if (cargoWeight.compareTo(BigDecimal.ZERO) == 0) {
            for (LineItem lineItem : lineItems) {
                BigDecimal unitQuantity = lineItem.getUnitQuantity();
                BigDecimal unitWeight = lineItem.getUnitWeight();
                if (unitQuantity != null && unitWeight != null) {
                    totalLineItemWeight = totalLineItemWeight.add(unitQuantity.multiply(unitWeight));
                }
            }
        } else {
            totalLineItemWeight = cargoWeight;
        }
        return totalLineItemWeight;
    }

    // TODO. Move this out. Make it a query. This is SLOW. It is killing reports.
    @Deprecated
    public BigDecimal getTotalLineItemVolume() {
        BigDecimal totalLineItemVolume = BigDecimal.ZERO;
        BigDecimal cargoVolume = BigDecimal.ZERO;

        if (consignment != null && consignment.getContainers() != null) {
            for (Container container : consignment.getContainers()) {
                if (container.getVolumeM3() != null)
                    cargoVolume = cargoVolume.add(container.getVolumeM3());
            }
        }

        if (cargoVolume.compareTo(BigDecimal.ZERO) == 0) {
            for (LineItem lineItem : lineItems) {
                BigDecimal unitQuantity = lineItem.getUnitQuantity();
                BigDecimal unitVolume = lineItem.getUnitVolume();
                if (unitQuantity != null && unitVolume != null) {
                    totalLineItemVolume = totalLineItemVolume.add(unitQuantity.multiply(unitVolume));
                }
            }
        } else {
            totalLineItemVolume = cargoVolume;
        }
        return totalLineItemVolume;
    }

    @Override
    public List<AddedComment> getComments() {
        return comments;
    }

    @Override
    public void setComments(List<AddedComment> comments) {
        this.comments = comments;
    }

    public String getProFormaReference() {
        return proFormaReference;
    }

    public void setProFormaReference(String proFormaReference) {
        this.proFormaReference = proFormaReference;
    }

    public BigDecimal getTotalInvoiceValue() {
        if (totalInvoiceValue == null) {
            calculateTotalInvoiceValue();
        }
        return totalInvoiceValue;
    }

    public void setTotalInvoiceValue(BigDecimal totalInvoiceValue) {
        this.totalInvoiceValue = totalInvoiceValue;
    }

    @Transient
    @Override
    public BigDecimal getTotalSettleableValue() {
        return getTotalInvoiceValue();
    }

    @Override
    public BigDecimal getGrossValue() {
        return getTotalInvoiceValue();
    }

    // TODO. Move this out. Make it a query.
    public void calculateTotalInvoiceValue() {
        BigDecimal total = new BigDecimal("0.00");
        for (LineItem lineItem : getLineItems()) {
            if (!lineItem.isAdditional()) {
                // Only include items added at estimate level i.e. where "additional" is false
                total = total.add(ObjectUtils.firstNonNull(lineItem.getTotalCost(), BigDecimal.ZERO));
            }
        }
        totalInvoiceValue = total;
    }

    public UnitPricePerItem getUnitPricePerItem() {
        return unitPricePerItem;
    }

    public void setUnitPricePerItem(UnitPricePerItem unitPricePerItem) {
        this.unitPricePerItem = unitPricePerItem;
    }

    public OrderLetterOfCredit getOrderLetterOfCredit() {
        return orderLetterOfCredit;
    }

    public void setOrderLetterOfCredit(OrderLetterOfCredit orderLetterOfCredit) {
        this.orderLetterOfCredit = orderLetterOfCredit;
    }

    public String getLspBookingReference() {
        return lspBookingReference;
    }

    public void setLspBookingReference(String lspBookingReference) {
        this.lspBookingReference = lspBookingReference;
    }

    public String getVesselName() {
        return vesselName;
    }

    public void setVesselName(String vesselName) {
        this.vesselName = vesselName;
    }

    public ShipmentType getShipmentType() {
        return shipmentType;
    }

    public void setShipmentType(ShipmentType shipmentType) {
        this.shipmentType = shipmentType;
    }

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

    @Override
    public void setState(OrderState state) {
        if (!this.state.equals(state)) {
            this.stateDate = new Date();
        }
        this.state = state;
    }

    public BusinessState getBusinessState() {
        return businessState;
    }

    public void setBusinessState(BusinessState businessState) {
        this.businessState = businessState;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append("reference", orderReference).toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!HibernateUtils.proxyClassEquals(this, obj)) {
            return false;
        }
        Order other = (Order) HibernateUtils.initializeAndUnproxy(obj);
        return new EqualsBuilder().append(number, other.getNumber()).append(orderReference, other.getOrderReference()).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(number).append(orderReference).toHashCode();
    }

    public PaymentMethod getPaymentMethod() {
        return paymentMethod;
    }

    public void setPaymentMethod(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public PaymentTerm getPaymentTerm() {
        return paymentTerm;
    }

    public void setPaymentTerm(PaymentTerm paymentTerm) {
        this.paymentTerm = paymentTerm;
    }

    public BigDecimal getAmountAtIncoterm() {
        // TODO Auto-generated method stub
        return null;
    }

    public Currency getCostingCurrency() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Set<PlannedSettlement> getPlannedSettlements() {
        return Collections.unmodifiableSet(plannedSettlements);
    }

    @Override
    public void setPlannedSettlements(Set<PlannedSettlement> plannedSettlements) {
        this.plannedSettlements = plannedSettlements;
    }

    public List<PlannedSettlement> getPlannedSettlementList() {
        return Collections.unmodifiableList(new ArrayList<PlannedSettlement>(plannedSettlements));
    }

    public PlannedSettlementHelper getPlannedSettlementHelper() {
        return new PlannedSettlementHelper(getPlannedSettlements());
    }

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

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

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

    /**
     * Determines whether or not all the line items on this order are tariffed.
     *
     * Loops through each line item and invokes {@link LineItem#isTariffed()}.
     *
     * @return if any line item in {@link #lineItems} returns false for
     * {@link LineItem#isTariffed()}
     */
    @Deprecated
    public boolean allItemsTariffed() {
        // TODO. Use a query
        for (LineItem item : lineItems) {
            if (!item.isTariffed()) {
                return false;
            }
        }
        return true;
    }

    @Override
    public void accept(CostingVisitor costingVisitor) {
        for (LineItem lineItem : getActiveLineItems()) {
            lineItem.accept(costingVisitor);
        }
        costingVisitor.visit(this);
    }

    /**
     * Helper method for the view tier. If the order is in any of these states then we will not
     * allow editing of certain fields. Mostly on the Order Details tab and the Shipping tabs.
     */
    @Override
    public boolean inNonEditableState() {
        return NON_EDITABLE_STATES.contains(state) ||
                (consignment != null && consignment.getState() == ConsignmentState.AWAITING_TREASURY_RATES);
    }

    public boolean inNonSalesEditableState() {
        return SALE_NON_EDITABLE_STATES.contains(state);
    }

    /**
     * Gets the balance of the invoice gross value minus the current planned settlement total total.
     *
     * @return
     */
    public BigDecimal getBalance() {
        if (getTotalInvoiceValue() != null) {
            int scale = MathUtils.SCALE_FOUR;

            BigDecimal totalInvoiceValue = getTotalInvoiceValue();
            totalInvoiceValue = MathUtils.setScale(totalInvoiceValue, scale);

            BigDecimal plannedSettlementsTotalValue = getPlannedSettlementHelper().getTotalValue();
            plannedSettlementsTotalValue = MathUtils.setScale(plannedSettlementsTotalValue, scale);

            return totalInvoiceValue.subtract(plannedSettlementsTotalValue);
        }
        return null;
    }

    /**
     * add amount given to totalInvoiceValue.
     *
     * @param additionalCosts- amount to add.
     */
    public void addToCalculateTotalInvoiceValue(BigDecimal additionalCosts) {
        calculateTotalInvoiceValue();
        totalInvoiceValue = getTotalInvoiceValue().add(additionalCosts);
    }

    @Override
    public int compareTo(Order other) {
        return other.getCreated().compareTo(getCreated());
    }

    public Date getOriginalDocumentsReceivedDate() {
        return originalDocumentsReceivedDate;
    }

    public void setOriginalDocumentsReceivedDate(Date originalDocumentsReceivedDate) {
        this.originalDocumentsReceivedDate = originalDocumentsReceivedDate;
    }

    public boolean isIntegrated() {
        return integrated;
    }

    public void setIntegrated(boolean integrated) {
        this.integrated = integrated;
    }

    public String getDeletedReason() {
        return this.deletedReason;
    }

    public void setDeletedReason(String deletedReason) {
        this.deletedReason = deletedReason;
    }

    @Override
    public String getKey() {
        return new StringBuilder(toString()).append("-").append(hashCode()).toString();
    }

    public String getOldOrderReference() {
        return oldOrderReference;
    }

    public void setOldOrderReference(String oldOrderReference) {
        this.oldOrderReference = oldOrderReference;
    }

    public boolean orderReferenceChanged() {
        return (oldOrderReference != null && orderReference != null && !orderReference.equals(oldOrderReference));
    }

    public OrdersEvent getFirstEvent(OrderEventType orderEventType) {
        return Event.getFirstEvent(events, orderEventType);
    }

    public OrdersEvent getLastEvent(OrderEventType orderEventType) {
        List<OrdersEvent> copyEvents = new ArrayList(events);
        Collections.reverse(copyEvents);
        return Event.getFirstEvent(copyEvents, orderEventType);
    }

    public Date getAddedToConsignmentDate() {
        return addedToConsignmentDate;
    }

    public void setAddedToConsignmentDate(Date addedToConsignmentDate) {
        this.addedToConsignmentDate = addedToConsignmentDate;
    }

    /**
     * @return buyer or seller depending on the concrete order class.
     */
    abstract public Employee getResponsibleEmployee();

    abstract public void setResponsibleEmployee(Employee employee);

    public boolean isExposureRequired() {
        return exposureRequired;
    }

    public void setExposureRequired(boolean exposureRequired) {
        this.exposureRequired = exposureRequired;
    }

    public boolean isPreviouslyBooked() {
        return previouslyBooked;
    }

    public void setPreviouslyBooked(boolean previouslyBooked) {
        this.previouslyBooked = previouslyBooked;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public abstract boolean isPurchaseOrder();

    public abstract SupplierCommon getSupplierOrCustomer();

    public abstract Employee getBuyer();

    public abstract BigDecimal getTotalSalesValue();

    public boolean inSignedOffState() {
        return SIGNED_OFF_STATES.contains(state);
    }

    public boolean isConfirmed() {
        return confirmed;
    }

    public void setConfirmed(boolean confirmed) {
        this.confirmed = confirmed;
    }
//
//    public List<TradeFinance> getTradeFinance() {
//        return tradeFinance;
//    }
//
//    public void setTradeFinance(List<TradeFinance> tradeFinance) {
//        this.tradeFinance = tradeFinance;
//    }

    public SpecialRequirementConfig getSpecialRequirementConfig() {
        return specialRequirementConfig;
    }

    public void setSpecialRequirementConfig(SpecialRequirementConfig specialRequirementConfig) {
        this.specialRequirementConfig = specialRequirementConfig;
    }

    public String getSpecialRequirementOtherDescription() {
        return specialRequirementOtherDescription;
    }

    public void setSpecialRequirementOtherDescription(String specialRequirementOtherDescription) {
        this.specialRequirementOtherDescription = specialRequirementOtherDescription;
    }

    public Set<GoodsReceivedReceipt> getGoodsReceivedReceipts() {
        return goodsReceivedReceipts;
    }

    @Override
    public CommentType getCommentType() {
        return CommentType.SHIPMENT;
    }

    public SubShipment getSubShipment() {
        return subShipment;
    }

    public void setSubShipment(SubShipment subShipment) {
        this.subShipment = subShipment;
    }

    public boolean isElc() {
        return elc;
    }

    public void setElc(boolean elc) {
        this.elc = elc;
    }

    public boolean isNotElcOrder() {
        return !elc;
    }

    @Override
    public OrganisationalUnit getOrderOrganisationalUnit() {
        return organisationalUnit;
    }

    public EstimateCostSummary getCostingSummary() {
        return costingSummary;
    }

    public void setCostingSummary(EstimateCostSummary costingSummary) {
        this.costingSummary = costingSummary;
    }

    public Type getCostingType() {
        return isElc() ? Type.ELC : Type.FULL;
    }

    public static enum UnlinkedOrdersType {
        ELIGIBLE, INELIGIBLE
    }

    public Contact getSupplierContact() {
        return supplierContact;
    }

    public void setSupplierContact(Contact supplierContact) {
        this.supplierContact = supplierContact;
    }

    public boolean isTotalsInUse() {
        return totalsInUse;
    }

    public void setTotalsInUse(boolean totalsInUse) {
        this.totalsInUse = totalsInUse;
    }

    public TradeAgreementOption getDefaultTradeAgreementOption() {
        return defaultTradeAgreementOption;
    }

    public void setDefaultTradeAgreementOption(TradeAgreementOption defaultTradeAgreementOption) {
        this.defaultTradeAgreementOption = defaultTradeAgreementOption;
    }

    public ItemType getDefaultItemType() {
        return defaultItemType;
    }

    public void setDefaultItemType(ItemType defaultItemType) {
        this.defaultItemType = defaultItemType;
    }

    @Override
    public String getDocumentGroupName() {
        return DocumentManagementHardCoding.ORDER.name();
    }

    public String getDMSKey() {
        //return new StringBuilder().append(getId()).toString();
        return new StringBuilder().append(getOrderReference()).toString();
    }

    public Long getBulkOrderId() {
        return bulkOrderId;
    }

    public void setBulkOrderId(Long bulkOrderId) {
        this.bulkOrderId = bulkOrderId;
    }

    public boolean isBulkOrder() {
        return bulkOrder;
    }

    public void setBulkOrder(boolean bulkOrder) {
        this.bulkOrder = bulkOrder;
    }

    public BigDecimal getOrderQuantity() {
        return orderQuantity;
    }

    public BigDecimal getWeightedSellingPriceExclusive() {
        return weightedSellingPriceExclusive;
    }

    public Set<AdditionalNotes> getAdditionalNotes() {
        if (additionalNotes == null)
            additionalNotes = new HashSet<>();
        return additionalNotes;
    }

    public void setAdditionalNotes(Set<AdditionalNotes> additionalNotes) {
        this.additionalNotes = additionalNotes;
    }

    public abstract DocumentGroupState getDocumentGroupStatus();

    public abstract void setDocumentGroupStatus(DocumentGroupState documentGroupStatus);

    public String getQuoteReference() {
        return quoteReference;
    }

    public void setQuoteReference(String quoteReference) {
        this.quoteReference = quoteReference;
    }

    public boolean isUploaded() {
        return uploaded;
    }

    public void setUploaded(boolean uploaded) {
        this.uploaded = uploaded;
    }

    public boolean isEarlyOrder() {
        return earlyOrder;
    }

    public void setEarlyOrder(boolean earlyOrder) {
        this.earlyOrder = earlyOrder;
    }

    public OrganisationalUnit getCompany() {
        return company;
    }

    public void setCompany(OrganisationalUnit company) {
        this.company = company;
    }
}