Skip to content

Create One Purchase Order per Supplier From a Purchase Quotation ​

These two Groovy scripts split a purchase quotation by supplier. Each line names its supplier in Reference 3, and every time the quotation is saved, the first script keeps one purchase order per supplier in step with it. The second script does the same with purchase invoices, and can also set each invoice's currency.

Recommended pattern

These scripts are not tied to any customer's setup. You can paste them as they are and control them entirely through their parameters.

Class namesEAGenMultiplePurchaseOrdersFromQuotation, EAGenMultiplePurchaseInvoicesFromQuotationWithCurrency
ScriptsEAGenMultiplePurchaseOrdersFromQuotation.groovy, EAGenMultiplePurchaseInvoicesFromQuotationWithCurrency.groovy
Runs onPurchase Quotation, on Post Commit.
UsesParameter 2 (required) and Parameter 3 (optional). Leave Parameter 1 empty.
Shipped asSource to paste. It is not built into the system.

When to use it ​

A buyer collects prices for a list of items and records them on one purchase quotation (Purchases → Documents → Purchase Quotation). The cheapest supplier is not the same for every item: the toner is best from one supplier, the paper from another. The standard way to order would be to make one purchase order per supplier by hand and copy the right lines into each.

With these scripts, the buyer just picks the winning supplier on each line, in Reference 3, and saves. One purchase order per supplier appears, holding exactly that supplier's lines. Change a line's supplier and save again: the line moves to the other supplier's order, and an order that no longer has any lines is deleted.

How it works ​

  1. It chooses the lines. When Parameter 3 holds the code of a Criteria Definition, only the lines matching it are used. Otherwise every line is used. Lines with an empty Reference 3 are always left out.
  2. It groups the lines by supplier, taken from Reference 3.
  3. For each supplier it finds or creates the document. A purchase order (or invoice) whose Reference 5 is this quotation and whose supplier is this supplier is reused. If there is none, a new one is created.
  4. It fills the document. The field map in Parameter 2 is applied, the header is copied from the quotation (except the warehouse and the locator), and the field map is applied once more so that it wins. The supplier is set, and Reference 5 and From Document point at the quotation. The lines are replaced with copies of the supplier's quotation lines.
  5. It saves the document, and writes the document into Reference 4 of each of the quotation lines it used.
  6. It deletes the leftovers. Any purchase order (or invoice) whose Reference 5 is this quotation but whose supplier no longer appears on the quotation is deleted.

The invoice script (EAGenMultiplePurchaseInvoicesFromQuotationWithCurrency) also reads two fields from each line it copies: a non-zero Number 1 becomes the invoice's currency rate, and a currency in Reference 2 becomes the invoice's currency.

Reference 5 belongs to the script

Step 6 deletes every purchase order (or invoice) whose Reference 5 points at the quotation and whose supplier is gone. Do not use Reference 5 of those documents for anything else, and do not link a hand-made order to the quotation through it.

The generated documents must save on their own

Each order or invoice is saved with the full checks of a normal save. If one of them is refused, the quotation's save is refused too, with the generated document's reason after Error while running entity flow. A purchase invoice needs a warehouse, which is not copied from the quotation, so set it in the field map; otherwise the save stops with Field Warehouse is required, can not be empty.

Setting it up ​

  1. Open Administration → Display Customization → Entity Flow and create a flow with Target Type set to Purchase Quotation.
  2. In the Details grid, add a line with the action Post Commit, and paste the whole script from one of the files above into Groovy Script. Leave Class Name empty: it is filled in when you save.
  3. Fill in the parameters:
ParameterTitle shown after savingWhat to enter
Parameter 1Not used - leave emptyNothing.
Parameter 2Fields MapA field map from the quotation (source) to the new document (target), one target=source pair per line. It must at least set the book and the term of the new document. See Field Values Calculator.
Parameter 3Lines CriteriaOptional. The code of a Criteria Definition on Purchase Quotation that picks the lines to use.
  1. Save the flow. Class Name now shows com.namasoft.modules.supplychain.domain.utils.plugnplay.groovy.EAGenMultiplePurchaseOrdersFromQuotation (or the invoice script's name), and each parameter shows its title.

Example ​

For purchase orders, Parameter 2 sets the order's book and term:

book='PO01'
term='PO'

For purchase invoices, it also sets the warehouse:

book='PI01'
term='PI'
warehouse='01'

A quotation has three lines: toner and paper with supplier A in Reference 3, and folders with supplier B. Saving it creates two purchase orders: one for supplier A with two lines, one for supplier B with one line. Each quotation line's Reference 4 shows its order. When the folders line is changed to supplier A and the quotation is saved again, supplier A's order is updated to three lines and supplier B's order is deleted.

To order only some lines, create a Criteria Definition on Purchase Quotation, for example the textual criteria details.n1,GreaterThan,0,AND;, and put its code in Parameter 3. Then only the lines with a Number 1 greater than zero are ordered.

The scripts ​

The purchase order script, EAGenMultiplePurchaseOrdersFromQuotation:

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

import com.namasoft.common.fieldids.NotGeneratedFields
import com.namasoft.common.utilities.CollectionsUtility
import com.namasoft.infor.domainbase.util.actions.EAGenerateEntityFromEntityAction
import com.namasoft.infra.domainbase.common.approval.CriteriaDefinition
import com.namasoft.infra.domainbase.common.approval.LineIndices
import com.namasoft.infra.domainbase.common.criteria.CriteriaBuilder
import com.namasoft.infra.domainbase.datafields.EntityCodeDF
import com.namasoft.infra.domainbase.datafields.LongTextDF
import com.namasoft.infra.domainbase.entity.base.EntityAction
import com.namasoft.infra.domainbase.entity.base.EntityMediator
import com.namasoft.infra.domainbase.entity.generic.GenericReference
import com.namasoft.infra.domainbase.persistence.repos.FilterOnDimensions
import com.namasoft.infra.domainbase.persistence.repos.ListPageMatchingParameters
import com.namasoft.infra.domainbase.persistence.repos.Persister
import com.namasoft.infra.domainbase.util.Result
import com.namasoft.modules.supplychain.domain.details.PurchaseOrderLine
import com.namasoft.modules.supplychain.domain.details.PurchaseQuotationLine
import com.namasoft.modules.supplychain.domain.entities.PurchaseOrder
import com.namasoft.modules.supplychain.domain.entities.PurchaseQuotation
import com.namasoft.modules.supplychain.domain.utils.SCDocFromDocCopier
import com.namasoft.modules.supplychain.utils.SCEntityTypes

import static com.namasoft.common.fieldids.AllModulesFieldIDs.PurchaseOrder_supplier

public class EAGenMultiplePurchaseOrdersFromQuotation implements EntityAction<PurchaseQuotation> {
    @Override
    Result doAction(PurchaseQuotation doc, LongTextDF... parameters) {
        Result result = Result.createAccumulatingResult();
        CriteriaDefinition criteria = Persister.findByBusinessCode(CriteriaDefinition.class, EntityCodeDF.fromDataField(parameters[2]));
        def details = doc.getDetails()
        if (criteria != null) {
            LineIndices lineIndices = new LineIndices();
            criteria.isEntityMatching(doc, lineIndices);
            details = new ArrayList<PurchaseQuotationLine>();
            def indices = lineIndices.getIndices();
            if (indices == null)
                indices = new ArrayList<Integer>();
            for (def i : indices) {
                details.add(doc.getDetails().get(i));
            }
        }
        Map<GenericReference, List<PurchaseQuotationLine>> map = CollectionsUtility.mapList(details, { it.getRef3() });
        List<PurchaseOrder> orders = listOrders(doc)
        map.remove(null);
        for (Map.Entry<GenericReference, List<PurchaseQuotationLine>> e : map.entrySet()) {
            PurchaseOrder order = findOrder(e.getKey(), doc);
            if (order == null) {
                order = EntityMediator.createEntity(SCEntityTypes.PurchaseOrder());
            } else {
                order.startEditing().addToAccumulatingResult(result);
            }
            EAGenerateEntityFromEntityAction.copyFieldsFromSourceToTarget(doc, order, parameters[1].getPrimitiveValue());
            EntityMediator.copyCommonDataFromSourceToGeneraed(doc, order, order.getTerm(), order.getBook());
            SCDocFromDocCopier.copyHeaderDataExceptWareAndLocator(order, doc);
            EAGenerateEntityFromEntityAction.copyFieldsFromSourceToTarget(doc, order, parameters[1].getPrimitiveValue());
            order.setRef5(GenericReference.fromEntity(doc));
            order.setSupplier(GenericReference.toReal(e.getKey()));
            order.setFromDoc(GenericReference.fromEntity(doc));
            List<PurchaseOrderLine> old = new ArrayList<>(order.getDetails());
            order.getDetails().clear();
            for (PurchaseQuotationLine src : e.getValue()) {
                PurchaseOrderLine copy = old.isEmpty() ? order.createLine() : old.remove(0);
                SCDocFromDocCopier.copyLine(src, copy, doc);
                order.getDetails().add(copy);
            }
            EntityMediator.commitFromBusinessAction(order).addToAccumulatingResult(result);
            Persister.flush();
            for (PurchaseQuotationLine src : e.getValue()) {
                src.setRef4(order.toGenericReference());
            }
            orders.remove(order);
        }
        deleteOrders(orders, result);
        result
    }

    private List<PurchaseOrder> listOrders(PurchaseQuotation doc) {
        List<PurchaseOrder> existing = Persister.listPageMatching(PurchaseOrder.class,
                ListPageMatchingParameters.create().criteria(CriteriaBuilder.create().field(NotGeneratedFields.ref5 + ".id").equal(doc.getId()).build()));
        return existing;
    }

    private void deleteOrders(List<PurchaseOrder> orders, Result result) {
        for (PurchaseOrder order : orders) {
            EntityMediator.deleteEntityFromBusinessAction(order).addToAccumulatingResult(result);
        }
    }

    private PurchaseOrder findOrder(GenericReference supplier, PurchaseQuotation doc) {
        return Persister.findFirstRecordMatching(PurchaseOrder.class, CriteriaBuilder.create().field(NotGeneratedFields.ref5 + ".id").equal(doc.getId()).and().field(PurchaseOrder_supplier).equal(GenericReference.toReal(supplier)).build(), FilterOnDimensions.No);
    }

    @Override
    String describe() {
        return "Purchase quotation: creates one purchase order per supplier, taking the supplier from each line's Ref 3. " +
                "Only the lines that match the criteria definition whose code is in parameter 3 are used (all lines when it is empty). " +
                "Each run updates the orders made before for the same suppliers, creates the missing ones and deletes the ones whose supplier is no longer used. " +
                "Header and lines are copied from the quotation, then the field map in parameter 2 is applied. " +
                "Each order's Ref 5 and From Document point at the quotation, and each used quotation line's Ref 4 points at its order. Parameter 1 is not used.";
    }

    @Override
    List<String> columnNames() {
        return Arrays.asList("Not used - leave empty", "Fields Map", "Lines Criteria")
    }
}

The purchase invoice script, EAGenMultiplePurchaseInvoicesFromQuotationWithCurrency:

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

import com.namasoft.common.fieldids.NotGeneratedFields
import com.namasoft.common.utilities.CollectionsUtility
import com.namasoft.infor.domainbase.util.actions.EAGenerateEntityFromEntityAction
import com.namasoft.infra.domainbase.common.approval.CriteriaDefinition
import com.namasoft.infra.domainbase.common.approval.LineIndices
import com.namasoft.infra.domainbase.common.criteria.CriteriaBuilder
import com.namasoft.infra.domainbase.datafields.DecimalDF
import com.namasoft.infra.domainbase.datafields.EntityCodeDF
import com.namasoft.infra.domainbase.datafields.LongTextDF
import com.namasoft.infra.domainbase.entity.base.EntityAction
import com.namasoft.infra.domainbase.entity.base.EntityMediator
import com.namasoft.infra.domainbase.entity.generic.GenericReference
import com.namasoft.infra.domainbase.persistence.repos.FilterOnDimensions
import com.namasoft.infra.domainbase.persistence.repos.ListPageMatchingParameters
import com.namasoft.infra.domainbase.persistence.repos.Persister
import com.namasoft.infra.domainbase.util.Result
import com.namasoft.modules.basic.domain.entities.Currency
import com.namasoft.modules.supplychain.domain.details.PurchaseInvoiceLine
import com.namasoft.modules.supplychain.domain.details.PurchaseQuotationLine
import com.namasoft.modules.supplychain.domain.entities.PurchaseInvoice
import com.namasoft.modules.supplychain.domain.entities.PurchaseQuotation
import com.namasoft.modules.supplychain.domain.utils.SCDocFromDocCopier
import com.namasoft.modules.supplychain.utils.SCEntityTypes

import static com.namasoft.common.fieldids.AllModulesFieldIDs.PurchaseInvoice_supplier

public class EAGenMultiplePurchaseInvoicesFromQuotationWithCurrency implements EntityAction<PurchaseQuotation> {
    @Override
    Result doAction(PurchaseQuotation doc, LongTextDF... parameters) {
        Result result = Result.createAccumulatingResult();
        CriteriaDefinition criteria = Persister.findByBusinessCode(CriteriaDefinition.class, EntityCodeDF.fromDataField(parameters[2]));
        def details = doc.getDetails()
        if (criteria != null) {
            LineIndices lineIndices = new LineIndices();
            criteria.isEntityMatching(doc, lineIndices);
            details = new ArrayList<PurchaseQuotationLine>();
            def indices = lineIndices.getIndices();
            if (indices == null)
                indices = new ArrayList<Integer>();
            for (def i : indices) {
                details.add(doc.getDetails().get(i));
            }
        }
        Map<GenericReference, List<PurchaseQuotationLine>> map = CollectionsUtility.mapList(details, { it.getRef3() });
        List<PurchaseInvoice> orders = listOrders(doc)
        map.remove(null);
        for (Map.Entry<GenericReference, List<PurchaseQuotationLine>> e : map.entrySet()) {
            PurchaseInvoice order = findOrder(e.getKey(), doc);
            if (order == null) {
                order = EntityMediator.createEntity(SCEntityTypes.PurchaseInvoice());
            } else {
                order.startEditing().addToAccumulatingResult(result);
            }
            EAGenerateEntityFromEntityAction.copyFieldsFromSourceToTarget(doc, order, parameters[1].getPrimitiveValue());
            EntityMediator.copyCommonDataFromSourceToGeneraed(doc, order, order.getTerm(), order.getBook());
            SCDocFromDocCopier.copyHeaderDataExceptWareAndLocator(order, doc);
            EAGenerateEntityFromEntityAction.copyFieldsFromSourceToTarget(doc, order, parameters[1].getPrimitiveValue());
            order.setRef5(GenericReference.fromEntity(doc));
            order.setSupplier(GenericReference.toReal(e.getKey()));
            order.setFromDoc(GenericReference.fromEntity(doc));
            List<PurchaseInvoiceLine> old = new ArrayList<>(order.getDetails());
            order.getDetails().clear();
            for (PurchaseQuotationLine src : e.getValue()) {
                PurchaseInvoiceLine copy = old.isEmpty() ? order.createLine() : old.remove(0);
                SCDocFromDocCopier.copyLine(src, copy, doc);
                order.getDetails().add(copy);
                if (DecimalDF.isNotEmptyOrZero(src.getN1()))
                    order.getMoney().setCurrencyRate(src.getN1());

                def currency = GenericReference.toRealOrNull(src.getRef2(), Currency.class)
                if (currency)
                    order.getMoney().setCurrency(currency);
            }
            EntityMediator.commitFromBusinessAction(order).addToAccumulatingResult(result);
            Persister.flush();
            for (PurchaseQuotationLine src : e.getValue()) {
                src.setRef4(order.toGenericReference());
            }
            orders.remove(order);
        }
        deleteOrders(orders, result);
        result
    }

    private List<PurchaseInvoice> listOrders(PurchaseQuotation doc) {
        List<PurchaseInvoice> existing = Persister.listPageMatching(PurchaseInvoice.class,
                ListPageMatchingParameters.create().criteria(CriteriaBuilder.create().field(NotGeneratedFields.ref5 + ".id").equal(doc.getId()).build()));
        return existing;
    }

    private void deleteOrders(List<PurchaseInvoice> orders, Result result) {
        for (PurchaseInvoice order : orders) {
            EntityMediator.deleteEntityFromBusinessAction(order).addToAccumulatingResult(result);
        }
    }

    private PurchaseInvoice findOrder(GenericReference supplier, PurchaseQuotation doc) {
        return Persister.findFirstRecordMatching(PurchaseInvoice.class, CriteriaBuilder.create().field(NotGeneratedFields.ref5 + ".id").equal(doc.getId()).and().field(PurchaseInvoice_supplier).equal(GenericReference.toReal(supplier)).build(), FilterOnDimensions.No);
    }

    @Override
    String describe() {
        return "Purchase quotation: creates one purchase invoice per supplier, taking the supplier from each line's Ref 3. " +
                "Only the lines that match the criteria definition whose code is in parameter 3 are used (all lines when it is empty). " +
                "Each run updates the invoices made before for the same suppliers, creates the missing ones and deletes the ones whose supplier is no longer used. " +
                "Header and lines are copied from the quotation, then the field map in parameter 2 is applied. " +
                "A line's N1, when not zero, becomes the invoice's currency rate, and a currency in the line's Ref 2 becomes the invoice's currency. " +
                "Each invoice's Ref 5 and From Document point at the quotation, and each used quotation line's Ref 4 points at its invoice. Parameter 1 is not used.";
    }

    @Override
    List<String> columnNames() {
        return Arrays.asList("Not used - leave empty", "Fields Map", "Lines Criteria")
    }
}