Skip to content

Update Items' Warehouse Settings From a Purchase Return Request ​

This Groovy script uses the Purchase Return Request as a data-entry sheet for item settings. When the request is committed, each line's values are written into the item's dimensions grid, on the row for the line's warehouse, and the item is saved. Extra fields can be copied with a field map spread over the parameters.

Customer-specific example

Six copies are written into the script and cannot be switched off through parameters. From each request line:

Request line fieldGoes to the item's dimensions row
N1Min Quantity
N2Order Limit
N5Slow moving period
B1Suggested In Discounts And Offers
N3N1
N4N2

Keep these fields free for this purpose on the request, or change the fixed copies in the script before you paste it.

Class nameEAUpdateItemDimensionsFromPurchaseReturnReqLines
ScriptEAUpdateItemDimensionsFromPurchaseReturnReqLines.groovy
Runs onPurchase Return Request. Target action Post Commit.
UsesParameters 2 to 15 (all optional). Leave Parameter 1 empty.
Shipped asSource to paste. It is not built into the system.

When to use it ​

Each item's dimensions grid holds per-warehouse settings such as the minimum quantity and the order limit. Keeping hundreds of those rows up to date by opening one item at a time is slow. If buyers already review slow-moving stock line by line on a purchase return request, the request can carry the new settings as well: they type them on the lines, and committing the request updates every item at once.

How it works ​

For each request line that has an item:

  1. It finds the item's row. It looks in the item's dimensions grid for a row with the same item code and the same warehouse as the line. If there is none, it adds a new row with that item code and warehouse.
  2. It copies the fixed fields. The six fields in the table above are copied, but only when the request line has a value: a zero or empty number, or an empty B1, leaves the item's value as it was.
  3. It applies the field map. Every map line that starts with details. writes into that same row. Every map line that starts with sizesAndColors. writes into the item's Sizes and Colors row that has the request line's size and color; if the item has no such row, one is added.
  4. It saves each item once. After all lines are handled, every item touched is saved and committed, even when several lines point to it.

The field map ​

Write one mapping per line, in any of Parameters 2 to 15. Lines from all parameters are read together, so you can spread a long map over several parameters.

text
details.description2=text1
details.n3=n5
sizesAndColors.name1="Returned"
  • The left side is the target: details. or sizesAndColors. followed by a field id of that grid's row. A line without one of these two prefixes is ignored.
  • The right side is a field id of the request line, or a fixed text in double quotes.
  • When the request line's value is empty, that mapping is skipped for that line.

Setting it up ​

  1. Open Administration → Display Customization → Entity Flow and create a flow whose target type is Purchase Return Request.
  2. In the Details grid, add a line with Target Action set to Post Commit, and paste the whole script into its Groovy Script column. Leave Class Name empty: it is filled in when you save.
  3. Put the field map, if any, in Parameter 2 and onwards, and save the flow.
ParameterTitle shown after savingWhat to enter
Parameter 1Not used - leave emptyNothing.
Parameter 2Field map (optional), one per line: details.<field>=<line field> or sizesAndColors.<field>=<line field>Map lines, for example details.description2=text1.
Parameters 3 and 4More field map lines (optional)More map lines. Parameters 5 to 15 are read too; they just have no title.

The items are really saved

The item records are changed and committed as part of committing the request. If an item refuses the change because one of its own validations fails, that item's message is returned when the request is committed.

Example ​

Parameter 2 holds details.description2=text1. A request line for item PIPE-25 in warehouse MAIN has N1 = 5, N2 = 10, N3 = 7 and Text 1 Returned batch. After the request is committed, the item's dimensions row for MAIN shows Min Quantity 5, Order Limit 10, N1 7 and Description 2 Returned batch. If the item had no row for MAIN, that row has just been added.

The script ​

groovy
package com.namasoft.modules.supplychain.domain.utils.plugnplay.groovy

import com.namasoft.common.utilities.ObjectChecker
import com.namasoft.infra.domainbase.datafields.DecimalDF
import com.namasoft.infra.domainbase.datafields.LongTextDF
import com.namasoft.infra.domainbase.datafields.UniqueTextDF
import com.namasoft.infra.domainbase.entity.base.EntityAction
import com.namasoft.infra.domainbase.entity.base.EntityMediator
import com.namasoft.infra.domainbase.metadata.EntityReflection
import com.namasoft.infra.domainbase.persistence.repos.Persister
import com.namasoft.infra.domainbase.util.Result
import com.namasoft.modules.supplychain.domain.details.ItemDimensionsDetail
import com.namasoft.modules.supplychain.domain.details.PurchaseReturnReqLine
import com.namasoft.modules.supplychain.domain.details.SizesAndColors
import com.namasoft.modules.supplychain.domain.entities.InvItem
import com.namasoft.modules.supplychain.domain.entities.PurchaseReturnReq
import com.namasoft.modules.supplychain.domain.primitives.ItemColorDF
import com.namasoft.modules.supplychain.domain.primitives.ItemSizeDF
import com.namasoft.modules.supplychain.domain.valueobjects.ItemDimensions
import com.namasoft.modules.supplychain.domain.valueobjects.ItemDimensionsData

public class EAUpdateItemDimensionsFromPurchaseReturnReqLines implements EntityAction<PurchaseReturnReq> {

    @Override
    Result doAction(PurchaseReturnReq object, LongTextDF... parameters) {
        Result result = Result.createAccumulatingResult();
        Set<InvItem> items = new LinkedHashSet<>();
        for (PurchaseReturnReqLine requestLine : object.getDetails()) {
            InvItem item = loadItem(requestLine);
            if (ObjectChecker.isEmptyOrNull(item))
                continue;
            upsertItemDimensionFromRequestLine(item, requestLine, parameters);
            applySizeAndColorMappings(item, requestLine, parameters);
            items.add(item);
        }
        for (InvItem item : items) {
            item.startEditing().addToAccumulatingResult(result);
            EntityMediator.commitFromBusinessAction(item).addToAccumulatingResult(result);
        }
        return result;
    }

    private InvItem loadItem(PurchaseReturnReqLine line) {
        if (ObjectChecker.isEmptyOrNull(line.getItem()) || ObjectChecker.isEmptyOrNull(line.getItem().getItem()))
            return null;
        return Persister.findByID(InvItem.class, line.getItem().getItem().getId());
    }

    private void upsertItemDimensionFromRequestLine(InvItem item, PurchaseReturnReqLine requestLine, LongTextDF... parameters) {
        ItemDimensionsDetail dimension = findOrAddDimension(item, requestLine);
        copyNotEmptyFieldsFromRequestLine(requestLine, dimension);
        applyMappings(requestLine, dimension, parseFieldMappings(DETAILS_PREFIX, parameters));
    }

    private ItemDimensionsDetail findOrAddDimension(InvItem item, PurchaseReturnReqLine requestLine) {
        ItemDimensionsDetail dimension = null;
        if (ObjectChecker.isNotEmptyOrNull(item.getDetails()))
            dimension = item.getDetails().stream().filter(dim -> ObjectChecker.isNotEmptyOrNull(dim.getItemCode()) && matches(requestLine, dim)).findFirst().orElse(null);
        if (dimension == null)
            dimension = addNewDimension(item, requestLine);
        return dimension;
    }

    private void applySizeAndColorMappings(InvItem item, PurchaseReturnReqLine requestLine, LongTextDF... parameters) {
        List<String[]> mappings = parseFieldMappings(SIZES_AND_COLORS_PREFIX, parameters);
        if (ObjectChecker.isEmptyOrNull(mappings))
            return;
        SizesAndColors sizeAndColor = findOrAddSizeAndColor(item, requestLine);
        applyMappings(requestLine, sizeAndColor, mappings);
    }

    private static void applyMappings(PurchaseReturnReqLine requestLine, Object targetLine, List<String[]> mappings) {
        for (String[] mapping : mappings) {
            Object value = resolveSourceValue(requestLine, mapping[1]);
            if (ObjectChecker.isNotEmptyOrNull(value))
                EntityReflection.setIncompatibleValueInField(targetLine, mapping[0], value);
        }
    }

    private static final String SIZES_AND_COLORS_PREFIX = "sizesAndColors.";
    private static final String DETAILS_PREFIX = "details.";

    private static List<String[]> parseFieldMappings(String prefix, LongTextDF... parameters) {
        List<String[]> mappings = new ArrayList<>();
        for (LongTextDF parameter : parameters) {
            if (parameter == null || ObjectChecker.isEmptyOrNull(parameter.getPrimitiveValue()))
                continue;
            for (String rawLine : parameter.getPrimitiveValue().split("\\r?\\n")) {
                int separator = rawLine.indexOf("=");
                if (separator <= 0)
                    continue;
                String targetField = rawLine.substring(0, separator).trim();
                String sourceField = rawLine.substring(separator + 1).trim();
                if (!hasPrefix(targetField, prefix))
                    continue;
                targetField = targetField.substring(prefix.length()).trim();
                if (ObjectChecker.isNotEmptyOrNull(targetField) && ObjectChecker.isNotEmptyOrNull(sourceField))
                    mappings.add([targetField, sourceField] as String[]);
            }
        }
        return mappings;
    }

    private static boolean hasPrefix(String targetField, String prefix) {
        return ObjectChecker.isNotEmptyOrNull(targetField) && targetField.regionMatches(true, 0, prefix, 0, prefix.length());
    }

    private static Object resolveSourceValue(PurchaseReturnReqLine requestLine, String sourceField) {
        if (sourceField.length() >= 2 && sourceField.startsWith("\"") && sourceField.endsWith("\""))
            return sourceField.substring(1, sourceField.length() - 1);
        return EntityReflection.getFieldValue(sourceField, requestLine, true);
    }

    private SizesAndColors findOrAddSizeAndColor(InvItem item, PurchaseReturnReqLine requestLine) {
        String sizeCode = sizeCode(requestLine);
        String colorCode = colorCode(requestLine);
        SizesAndColors sizeAndColor = null;
        if (ObjectChecker.isNotEmptyOrNull(item.getSizesAndColors()))
            sizeAndColor = item.getSizesAndColors().stream().filter(sc -> matchesSizeAndColor(sc, sizeCode, colorCode)).findFirst().orElse(null);
        if (sizeAndColor == null) {
            if (item.getSizesAndColors() == null)
                item.setSizesAndColors(new ArrayList<SizesAndColors>());
            sizeAndColor = new SizesAndColors();
            sizeAndColor.setSize(ItemSizeDF.fromString(sizeCode));
            sizeAndColor.setColor(ItemColorDF.fromString(colorCode));
            item.getSizesAndColors().add(sizeAndColor);
        }
        return sizeAndColor;
    }

    private static boolean matchesSizeAndColor(SizesAndColors sizeAndColor, String sizeCode, String colorCode) {
        return ObjectChecker.areEqual(sizeCode, primitive(sizeAndColor.getSize())) &&
                ObjectChecker.areEqual(colorCode, primitive(sizeAndColor.getColor()));
    }

    private static String sizeCode(PurchaseReturnReqLine line) {
        return line.getSpecificDimensions() == null ? null : primitive(line.getSpecificDimensions().getSize());
    }

    private static String colorCode(PurchaseReturnReqLine line) {
        return line.getSpecificDimensions() == null ? null : primitive(line.getSpecificDimensions().getColor());
    }

    private ItemDimensionsDetail addNewDimension(InvItem item, PurchaseReturnReqLine requestLine) {
        if (item.getDetails() == null)
            item.setDetails(new ArrayList<ItemDimensionsDetail>());
        ItemDimensionsDetail dimension = new ItemDimensionsDetail();
        dimension.setItemCode(UniqueTextDF.fromString(itemCode(requestLine)));
        ItemDimensions dimensions = new ItemDimensions();
        dimensions.getSpecificDimensions().setWarehouse(requestLine.fetchWarehouse());
        dimension.setItemDimensions(dimensions);
        item.getDetails().add(dimension);
        return dimension;
    }

    private void copyNotEmptyFieldsFromRequestLine(PurchaseReturnReqLine line, ItemDimensionsDetail dim) {
        ItemDimensionsData data = ensureData(dim);
        if (DecimalDF.isNotEmptyOrZero(line.getN1()))
            data.setMinQuantity(line.getN1());
        if (DecimalDF.isNotEmptyOrZero(line.getN2()))
            data.setOrderLimit(line.getN2());
        if (DecimalDF.isNotEmptyOrZero(line.getN5()))
            data.setSlowMovingPeriod(line.getN5());
        if (ObjectChecker.isNotEmptyOrNull(line.getB1()))
            data.setSuggestedDiscsNOffers(line.getB1());
        if (DecimalDF.isNotEmptyOrZero(line.getN3()))
            dim.setN1(line.getN3());
        if (DecimalDF.isNotEmptyOrZero(line.getN4()))
            dim.setN2(line.getN4());
    }

    private ItemDimensionsData ensureData(ItemDimensionsDetail dim) {
        if (dim.getData() == null)
            dim.setData(new ItemDimensionsData());
        return dim.getData();
    }

    private boolean matches(PurchaseReturnReqLine line, ItemDimensionsDetail dim) {
        return ObjectChecker.areEqual(line.fetchWarehouse(), dim.fetchWarehouse()) &&
                ObjectChecker.areEqual(itemCode(line), primitive(dim.getItemCode()));
    }

    private static String itemCode(PurchaseReturnReqLine line) {
        return line.getItem() == null ? null : primitive(line.getItem().getItemCode());
    }

    private static String primitive(def dataField) {
        return dataField == null ? null : dataField.getPrimitiveValue();
    }

    @Override
    String describe() {
        return "For each Purchase Return Request line, updates the matching item dimensions line on the item (matched by warehouse + item code) using only the line's non-empty values, then saves and commits the item. If no dimensions line matches that code + warehouse, a new one is added.\n" +
                "Fixed copies: request line n1 -> min quantity, n2 -> order limit, n5 -> slow moving period, b1 -> suggested in discounts and offers, n3 -> dimensions line n1, n4 -> dimensions line n2.\n" +
                "Parameters (optional): a field map that fills the item's detail lines. One mapping per line, format:\n" +
                "<listPrefix><targetField>=<requestLineSourceField>\n" +
                "Supported list prefixes:\n" +
                "  details.         -> the Item Dimensions line (matched by warehouse + item code)\n" +
                "  sizesAndColors.  -> the Sizes And Colors row (matched by the request line's size + color, added when missing)\n" +
                "A line whose target has no supported prefix is ignored. Mixed example:\n" +
                "details.description2=text1\n" +
                "details.n3=n5\n" +
                "sizesAndColors.text3=text1\n" +
                "sizesAndColors.n1=n3\n" +
                "sizesAndColors.name1=\"Returned\"\n" +
                "The right-hand side is a field id on the request line, or a \"quoted literal\". Empty source values are skipped. Mappings may be spread across parameters 2 to 15; leave parameter 1 empty."
    }

    @Override
    List<String> columnNames() {
        return Arrays.asList("Not used - leave empty", "Field map (optional), one per line: details.<field>=<line field> or sizesAndColors.<field>=<line field>",
                "More field map lines (optional)", "More field map lines (optional)");
    }
}