de.metas.ui.web.order.sales.purchasePlanning.view.PurchaseRow.java Source code

Java tutorial

Introduction

Here is the source code for de.metas.ui.web.order.sales.purchasePlanning.view.PurchaseRow.java

Source

package de.metas.ui.web.order.sales.purchasePlanning.view;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import org.adempiere.exceptions.AdempiereException;
import org.compiere.model.I_C_UOM;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

import de.metas.bpartner.BPartnerId;
import de.metas.money.Money;
import de.metas.printing.esb.base.util.Check;
import de.metas.product.ProductId;
import de.metas.purchasecandidate.DemandGroupReference;
import de.metas.purchasecandidate.PurchaseCandidatesGroup;
import de.metas.purchasecandidate.PurchaseCandidatesGroup.PurchaseCandidatesGroupBuilder;
import de.metas.purchasecandidate.PurchaseDemand;
import de.metas.purchasecandidate.availability.AvailabilityResult;
import de.metas.purchasecandidate.availability.AvailabilityResult.Type;
import de.metas.purchasecandidate.grossprofit.PurchaseProfitInfo;
import de.metas.purchasecandidate.grossprofit.PurchaseProfitInfoRequest;
import de.metas.purchasecandidate.grossprofit.PurchaseProfitInfoService;
import de.metas.purchasecandidate.model.I_C_PurchaseCandidate;
import de.metas.quantity.Quantity;
import de.metas.ui.web.exceptions.EntityNotFoundException;
import de.metas.ui.web.view.IViewRow;
import de.metas.ui.web.view.descriptor.annotation.ViewColumn;
import de.metas.ui.web.view.descriptor.annotation.ViewColumnHelper;
import de.metas.ui.web.window.datatypes.DocumentId;
import de.metas.ui.web.window.datatypes.DocumentPath;
import de.metas.ui.web.window.datatypes.LookupValue;
import de.metas.ui.web.window.descriptor.DocumentFieldWidgetType;
import de.metas.ui.web.window.descriptor.ViewEditorRenderMode;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;

/*
 * #%L
 * metasfresh-webui-api
 * %%
 * Copyright (C) 2017 metas GmbH
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

@ToString(doNotUseGetters = true)
public final class PurchaseRow implements IViewRow {
    // services
    @Nullable
    private final PurchaseProfitInfoService purchaseProfitInfoService;
    @Nullable
    private final PurchaseRowLookups lookups;

    public static final String FIELDNAME_QtyToPurchase = "qtyToPurchase";
    public static final String FIELDNAME_PurchasedQty = "purchasedQty";
    public static final String FIELDNAME_DatePromised = "datePromised";

    //
    @ViewColumn(captionKey = "M_Product_ID", widgetType = DocumentFieldWidgetType.Lookup, seqNo = 10)
    private final LookupValue product;

    /** TODO: show it if/when it's needed and QAed */
    // @ViewColumn(captionKey = "M_AttributeSetInstance_ID", widgetType = DocumentFieldWidgetType.Lookup, seqNo = 15)
    private final LookupValue attributeSetInstance;

    @ViewColumn(captionKey = "Vendor_ID", widgetType = DocumentFieldWidgetType.Lookup, seqNo = 20)
    private final LookupValue vendorBPartner;

    @ViewColumn(captionKey = I_C_PurchaseCandidate.COLUMNNAME_ProfitSalesPriceActual, widgetType = DocumentFieldWidgetType.Amount, seqNo = 23)
    private Money profitSalesPriceActual;

    @ViewColumn(captionKey = I_C_PurchaseCandidate.COLUMNNAME_ProfitPurchasePriceActual, widgetType = DocumentFieldWidgetType.Amount, seqNo = 25)
    private Money profitPurchasePriceActual;

    @ViewColumn(captionKey = "PercentGrossProfit", widgetType = DocumentFieldWidgetType.Amount, seqNo = 25)
    private BigDecimal profitPercent;

    @ViewColumn(captionKey = "Qty_AvailableToPromise", widgetType = DocumentFieldWidgetType.Quantity, seqNo = 30)
    private final Quantity qtyAvailableToPromise;

    @ViewColumn(captionKey = "QtyToDeliver", widgetType = DocumentFieldWidgetType.Quantity, seqNo = 40)
    private final Quantity qtyToDeliver;

    @ViewColumn(fieldName = FIELDNAME_QtyToPurchase, captionKey = "QtyToPurchase", widgetType = DocumentFieldWidgetType.Quantity, seqNo = 50)
    @Getter
    private Quantity qtyToPurchase;

    @ViewColumn(fieldName = FIELDNAME_PurchasedQty, captionKey = "PurchasedQty", widgetType = DocumentFieldWidgetType.Quantity, seqNo = 55)
    @Getter(AccessLevel.PRIVATE)
    private Quantity purchasedQty;

    @ViewColumn(captionKey = "C_UOM_ID", widgetType = DocumentFieldWidgetType.Text, seqNo = 60)
    private String uomOrAvailablility;

    @ViewColumn(fieldName = FIELDNAME_DatePromised, captionKey = "DatePromised", widgetType = DocumentFieldWidgetType.DateTime, seqNo = 70)
    @Getter
    private LocalDateTime datePromised;

    //
    private final PurchaseRowId rowId;

    private ImmutableMap<PurchaseRowId, PurchaseRow> _includedRowsById = ImmutableMap.of();

    private final boolean readonly;

    @Getter(AccessLevel.PRIVATE)
    private PurchaseCandidatesGroup purchaseCandidatesGroup;

    private transient ImmutableMap<String, Object> _fieldNameAndJsonValues; // lazy

    private static final ImmutableMap<String, ViewEditorRenderMode> ViewEditorRenderModeByFieldName_ReadOnly = //
            ImmutableMap.<String, ViewEditorRenderMode>builder()
                    .put(FIELDNAME_QtyToPurchase, ViewEditorRenderMode.NEVER)
                    .put(FIELDNAME_DatePromised, ViewEditorRenderMode.NEVER).build();
    private static final ImmutableMap<String, ViewEditorRenderMode> ViewEditorRenderModeByFieldName_Editable = //
            ImmutableMap.<String, ViewEditorRenderMode>builder()
                    .put(FIELDNAME_QtyToPurchase, ViewEditorRenderMode.ALWAYS)
                    .put(FIELDNAME_DatePromised, ViewEditorRenderMode.ALWAYS).build();

    @Builder(builderMethodName = "groupRowBuilder", builderClassName = "GroupRowBuilder")
    private PurchaseRow(@NonNull final PurchaseDemand demand, @NonNull final Quantity qtyAvailableToPromise,
            @NonNull final List<PurchaseRow> includedRows,
            //
            @NonNull final PurchaseRowLookups lookups) {
        purchaseProfitInfoService = null; // not needed
        this.lookups = lookups;

        rowId = PurchaseRowId.groupId(demand.getId());

        product = lookups.createProductLookupValue(demand.getProductId());
        attributeSetInstance = lookups.createASILookupValue(demand.getAttributeSetInstanceId());

        vendorBPartner = null;

        final Quantity qtyToDeliver = demand.getQtyToDeliver();
        uomOrAvailablility = qtyToDeliver.getUOMSymbol();
        this.qtyAvailableToPromise = qtyAvailableToPromise;
        this.qtyToDeliver = qtyToDeliver;
        qtyToPurchase = null;
        purchasedQty = null;

        purchaseCandidatesGroup = null;
        profitSalesPriceActual = null;
        profitPurchasePriceActual = null;
        profitPercent = null;

        datePromised = demand.getSalesPreparationDate();

        readonly = true;

        setIncludedRows(ImmutableList.copyOf(includedRows));
        updateQtysFromIncludedRows();
    }

    @Builder(builderMethodName = "lineRowBuilder", builderClassName = "LineRowBuilder")
    private PurchaseRow(@NonNull final PurchaseCandidatesGroup purchaseCandidatesGroup,
            //
            @NonNull final PurchaseRowLookups lookups,
            @NonNull final PurchaseProfitInfoService purchaseProfitInfoService) {
        this.purchaseProfitInfoService = purchaseProfitInfoService;
        this.lookups = lookups;

        final BPartnerId vendorId = purchaseCandidatesGroup.getVendorId();
        final ProductId productId = purchaseCandidatesGroup.getProductId();

        readonly = purchaseCandidatesGroup.isReadonly();
        rowId = PurchaseRowId.lineId(purchaseCandidatesGroup.getPurchaseDemandId(), vendorId, readonly);

        product = lookups.createProductLookupValue(productId);
        attributeSetInstance = null;

        vendorBPartner = lookups.createBPartnerLookupValue(vendorId);

        qtyAvailableToPromise = null;
        qtyToDeliver = null;

        // Keep it last (like all setters called from ctor)
        setPurchaseCandidatesGroup(purchaseCandidatesGroup);
    }

    @Builder(builderMethodName = "availabilityDetailSuccessBuilder", builderClassName = "availabilityDetailSuccessBuilder")
    private PurchaseRow(@NonNull final AvailabilityResult availabilityResult, @NonNull final PurchaseRow lineRow) {
        purchaseProfitInfoService = null; // not needed
        lookups = null; // not needed

        rowId = lineRow.getRowId().withAvailabilityAndRandomDistinguisher(availabilityResult.getType());

        product = lineRow.product;
        attributeSetInstance = lineRow.attributeSetInstance;

        vendorBPartner = null;

        final String availabilityText = !Check.isEmpty(availabilityResult.getAvailabilityText(), true)
                ? availabilityResult.getAvailabilityText()
                : availabilityResult.getType().translate();
        uomOrAvailablility = availabilityText;
        qtyAvailableToPromise = null;
        qtyToDeliver = null;
        qtyToPurchase = availabilityResult.getQty();
        purchasedQty = null;

        purchaseCandidatesGroup = null;
        profitSalesPriceActual = null;
        profitPurchasePriceActual = null;
        profitPercent = null;

        datePromised = availabilityResult.getDatePromised();

        readonly = true;
    }

    @Builder(builderMethodName = "availabilityDetailErrorBuilder", builderClassName = "availabilityDetailErrorBuilder")
    private PurchaseRow(@NonNull final Throwable throwable, @NonNull final PurchaseRow lineRow) {
        purchaseProfitInfoService = null; // not needed
        lookups = null; // not needed

        rowId = lineRow.getRowId().withAvailabilityAndRandomDistinguisher(Type.NOT_AVAILABLE);

        product = lineRow.product;
        attributeSetInstance = lineRow.attributeSetInstance;

        vendorBPartner = null;

        uomOrAvailablility = AdempiereException.extractMessage(throwable);
        qtyAvailableToPromise = null;
        qtyToDeliver = null;
        qtyToPurchase = null;
        purchasedQty = null;

        purchaseCandidatesGroup = null;
        profitSalesPriceActual = null;
        profitPurchasePriceActual = null;
        profitPercent = null;

        datePromised = null;

        readonly = true;
    }

    private PurchaseRow(@NonNull final PurchaseRow from) {
        purchaseProfitInfoService = from.purchaseProfitInfoService;
        lookups = from.lookups;

        rowId = from.rowId;
        product = from.product;
        attributeSetInstance = from.attributeSetInstance;
        vendorBPartner = from.vendorBPartner;
        qtyAvailableToPromise = from.qtyAvailableToPromise;
        uomOrAvailablility = from.uomOrAvailablility;

        purchaseCandidatesGroup = from.purchaseCandidatesGroup;
        profitSalesPriceActual = from.profitSalesPriceActual;
        profitPurchasePriceActual = from.profitPurchasePriceActual;
        profitPercent = from.profitPercent;

        qtyToDeliver = from.qtyToDeliver;
        qtyToPurchase = from.qtyToPurchase;
        purchasedQty = from.purchasedQty;
        datePromised = from.datePromised;

        setIncludedRows(
                from.getIncludedRows().stream().map(PurchaseRow::copy).collect(ImmutableList.toImmutableList()));

        readonly = from.readonly;

        _fieldNameAndJsonValues = from._fieldNameAndJsonValues;
    }

    public PurchaseRow copy() {
        return new PurchaseRow(this);
    }

    public PurchaseRowId getRowId() {
        return rowId;
    }

    @Override
    public DocumentId getId() {
        return getRowId().toDocumentId();
    }

    public List<DemandGroupReference> getDemandGroupReferences() {
        Check.assume(PurchaseRowType.LINE.equals(getType()),
                "only 'line'-type rows have demandGroupReferences; this={}", this);
        return purchaseCandidatesGroup.getDemandGroupReferences();
    }

    @Override
    public PurchaseRowType getType() {
        return getRowId().getType();
    }

    @Override
    public boolean isProcessed() {
        return readonly;
    }

    @Override
    public DocumentPath getDocumentPath() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Map<String, Object> getFieldNameAndJsonValues() {
        if (_fieldNameAndJsonValues == null) {
            _fieldNameAndJsonValues = ViewColumnHelper.extractJsonMap(this);
        }
        return _fieldNameAndJsonValues;
    }

    @Override
    public Map<String, DocumentFieldWidgetType> getWidgetTypesByFieldName() {
        return ViewColumnHelper.getWidgetTypesByFieldName(PurchaseRow.class);
    }

    @Override
    public Map<String, ViewEditorRenderMode> getViewEditorRenderModeByFieldName() {
        return readonly ? ViewEditorRenderModeByFieldName_ReadOnly : ViewEditorRenderModeByFieldName_Editable;
    }

    private void resetFieldNameAndJsonValues() {
        _fieldNameAndJsonValues = null;
    }

    @Override
    public Collection<PurchaseRow> getIncludedRows() {
        return _includedRowsById.values();
    }

    private void setIncludedRows(@NonNull final ImmutableList<PurchaseRow> includedRows) {
        _includedRowsById = Maps.uniqueIndex(includedRows, PurchaseRow::getRowId);
    }

    PurchaseRow getIncludedRowById(@NonNull final PurchaseRowId rowId) {
        final PurchaseRow includedRow = _includedRowsById.get(rowId);
        if (includedRow == null) {
            throw new EntityNotFoundException("Included row not found").appendParametersToMessage()
                    .setParameter("rowId", rowId).setParameter("this", this);
        }
        return includedRow;
    }

    private I_C_UOM getCurrentUOM() {
        return getQtyToPurchase().getUOM();
    }

    private void setPurchaseCandidatesGroup(@NonNull final PurchaseCandidatesGroup purchaseCandidatesGroup) {
        if (Objects.equals(this.purchaseCandidatesGroup, purchaseCandidatesGroup)) {
            return;
        }

        // NOTE: we assume purchaseCandidatesGroup's vendor and product wasn't changed!

        this.purchaseCandidatesGroup = purchaseCandidatesGroup;
        setQtyToPurchase(purchaseCandidatesGroup.getQtyToPurchase());
        setPurchasedQty(purchaseCandidatesGroup.getPurchasedQty());
        setProfitInfo(purchaseCandidatesGroup.getProfitInfoOrNull());
        setDatePromised(purchaseCandidatesGroup.getPurchaseDatePromised());
    }

    private void setQtyToPurchase(final Quantity qtyToPurchase) {
        if (Objects.equals(this.qtyToPurchase, qtyToPurchase)) {
            return;
        }

        this.qtyToPurchase = qtyToPurchase;
        uomOrAvailablility = qtyToPurchase != null ? lookups.createUOMLookupValue(qtyToPurchase.getUOM()) : null;

        resetFieldNameAndJsonValues();
    }

    private void setPurchasedQty(final Quantity purchasedQty) {
        if (Objects.equals(this.purchasedQty, purchasedQty)) {
            return;
        }

        this.purchasedQty = purchasedQty;
        resetFieldNameAndJsonValues();
    }

    private void setDatePromised(final LocalDateTime datePromised) {
        if (Objects.equals(this.datePromised, datePromised)) {
            return;
        }

        this.datePromised = datePromised;
        resetFieldNameAndJsonValues();
    }

    private void setProfitInfo(@Nullable final PurchaseProfitInfo profitInfo) {
        if (profitInfo != null) {
            profitSalesPriceActual = profitInfo.getProfitSalesPriceActual().orElse(null);
            profitPurchasePriceActual = profitInfo.getProfitPurchasePriceActual().orElse(null);
            profitPercent = profitInfo.getProfitPercent()
                    .map(percent -> percent.roundToHalf(RoundingMode.HALF_UP).getValue()).orElse(null);
        } else {
            profitSalesPriceActual = null;
            profitPurchasePriceActual = null;
            profitPercent = null;
        }

        resetFieldNameAndJsonValues();
    }

    private void updateQtysFromIncludedRows() {
        Quantity qtyToPurchaseSum = null;
        Quantity purchasedQtySum = null;
        for (final PurchaseRow includedRow : getIncludedRows()) {
            qtyToPurchaseSum = Quantity.addNullables(qtyToPurchaseSum, includedRow.getQtyToPurchase());
            purchasedQtySum = Quantity.addNullables(purchasedQtySum, includedRow.getPurchasedQty());
        }

        setQtyToPurchase(qtyToPurchaseSum);
        setPurchasedQty(purchasedQtySum);
    }

    private void assertRowEditable() {
        if (readonly) {
            throw new AdempiereException("readonly").setParameter("rowId", getRowId());
        }
    }

    public void assertRowType(@NonNull final PurchaseRowType expectedRowType) {
        getRowId().assertRowType(expectedRowType);
        final PurchaseRowType rowType = getType();
        if (rowType != expectedRowType) {
            throw new AdempiereException("Expected " + expectedRowType + " but it was " + rowType + ": " + this);
        }
    }

    void changeIncludedRow(@NonNull final PurchaseRowId includedRowId,
            @NonNull final PurchaseRowChangeRequest request) {
        assertRowType(PurchaseRowType.GROUP);

        final PurchaseRow lineRow = getIncludedRowById(includedRowId);
        lineRow.changeRow(request);

        updateQtysFromIncludedRows();
    }

    private void changeRow(@NonNull final PurchaseRowChangeRequest request) {
        assertRowType(PurchaseRowType.LINE);
        assertRowEditable();

        //
        final PurchaseCandidatesGroup candidatesGroup = getPurchaseCandidatesGroup();
        final PurchaseCandidatesGroupBuilder newCandidatesGroup = candidatesGroup.toBuilder();
        boolean hasChanges = false;

        //
        // QtyToPurchase
        final Quantity qtyToPurchase = request.getQtyToPurchase(this::getCurrentUOM);
        boolean qtyToPurchaseChanged = false;
        if (qtyToPurchase != null && !Objects.equals(candidatesGroup.getQtyToPurchase(), qtyToPurchase)) {
            newCandidatesGroup.qtyToPurchase(qtyToPurchase);
            qtyToPurchaseChanged = true;
            hasChanges = true;
        }

        //
        // PurchaseDatePromised
        final LocalDateTime purchaseDatePromised = request.getPurchaseDatePromised();
        if (purchaseDatePromised != null) {
            newCandidatesGroup.purchaseDatePromised(purchaseDatePromised);
            hasChanges = true;
        }

        //
        // Recompute Profit Info
        if (qtyToPurchaseChanged) {
            final PurchaseProfitInfo profitInfo = purchaseProfitInfoService
                    .calculateNoFail(PurchaseProfitInfoRequest.builder()
                            .salesOrderAndLineIds(candidatesGroup.getSalesOrderAndLineIds())
                            .qtyToPurchase(qtyToPurchase).vendorProductInfo(candidatesGroup.getVendorProductInfo())
                            .build());
            newCandidatesGroup.profitInfoOrNull(profitInfo);
            hasChanges = true;
        }

        //
        // Stop here if there were no changes
        if (!hasChanges) {
            return;
        }

        //
        setPurchaseCandidatesGroup(newCandidatesGroup.build());
    }

    public void setAvailabilityInfoRow(@NonNull final PurchaseRow availabilityResultRow) {
        setAvailabilityInfoRows(ImmutableList.of(availabilityResultRow));
    }

    public void setAvailabilityInfoRows(@NonNull final List<PurchaseRow> availabilityResultRows) {
        assertRowType(PurchaseRowType.LINE);
        availabilityResultRows.forEach(
                availabilityResultRow -> availabilityResultRow.assertRowType(PurchaseRowType.AVAILABILITY_DETAIL));

        // If there is at least one "available on vendor" row,
        // we shall order directly and not aggregate later on a Purchase Order.
        final boolean allowPOAggregation = availabilityResultRows.stream()
                .noneMatch(row -> row.getRowId().isAvailableOnVendor());

        setIncludedRows(ImmutableList.copyOf(availabilityResultRows));
        setPurchaseCandidatesGroup(getPurchaseCandidatesGroup().allowingPOAggregation(allowPOAggregation));
    }

    public Stream<PurchaseCandidatesGroup> streamPurchaseCandidatesGroup() {
        final Stream<PurchaseCandidatesGroup> includedRowsStream = getIncludedRows().stream()
                .flatMap(PurchaseRow::streamPurchaseCandidatesGroup);

        final PurchaseCandidatesGroup candidatesGroup = getPurchaseCandidatesGroup();
        if (candidatesGroup == null) {
            return includedRowsStream;
        } else {
            return Stream.concat(Stream.of(candidatesGroup), includedRowsStream);
        }
    }
}