Skip to content

Refuse a Document Whose Quantities Differ From Its From Document ​

This Groovy script refuses to save a supply chain document unless its quantities match its From Document exactly. It adds up the quantity of every item, unit by unit, on both documents, and reports each item whose totals differ.

Recommended pattern

This script is not tied to any customer's setup. It takes no parameters, so you can paste it as it is.

Class nameEAQtyMustMatchFromDocPerItem
ScriptEAQtyMustMatchFromDocPerItem.groovy
Runs onAny supply chain document, on Validate On Save.
UsesNo parameters. Leave Parameter 1 empty.
Shipped asSource to paste. It is not built into the system.

When to use it ​

Some documents must never be partial. An invoice made from a sales order that was agreed as a whole, a delivery that must carry everything that was picked, a transfer that must receive exactly what was sent. The usual checks stop a document from taking more than its source; they do not stop it from taking less, or from swapping one item for another.

This script makes the rule strict. The document saves only when every item appears in the same total quantity, in the same unit, as on its From Document. The lines do not have to match one to one: 3 units on one line of the source may be 1 and 2 on two lines of the new document.

How it works ​

  1. It reads the From Document. A document with no From Document is refused.
  2. It adds up both documents by item and unit: every line's quantity is added to the total of its item in its unit.
  3. It compares the totals. An item missing on one side, or present with a different total, is reported. All the differences are reported together, so the user sees the full list in one go.

Because it runs on Validate On Save, it checks a document when it is saved for real, not when it is saved as a draft.

Every document of the target type needs a From Document

The script refuses any document without one. Limit the flow to the documents that must follow the rule, with the book, the term or the flow's Criteria, so that documents typed by hand still save.

Setting it up ​

  1. Open Administration → Display Customization → Entity Flow and create a flow with Target Type set to the document to check, for example Sales Invoice.
  2. In the Details grid, add a line with the action Validate On Save, and paste the whole script from the file above into Groovy Script. Leave Class Name empty: it is filled in when you save.
  3. Leave every parameter empty. After saving, Parameter 1 shows the title Not used - leave empty.
  4. Save the flow. Class Name now shows com.namasoft.modules.supplychain.domain.utils.plugnplay.groovy.EAQtyMustMatchFromDocPerItem.

Example ​

A sales order holds 2 printers and 3 scanners. With the flow on Sales Invoice:

  • An invoice made from it with 2 printers and 2 scanners is refused for the scanner, with {0} the scanner, {1} 3 and {2} 2 in this message:

    The item {0} total quantity in from doc is {1} and in current document is {2}

  • An invoice with no From Document is refused with No from doc specified.

  • An invoice with 2 printers, and the scanners split over two lines of 1 and 2, saves.

Messages you may see ​

The script's own messages have no Arabic translation, so they appear in English on Arabic screens as well. The system puts its standard heading, Error while running entity flow {0} for entity {1} - {2}:- — «خطأ أثناء تشغيل مسار الكيان {0} للسجل {1} - {2}:-», in front of each, naming the flow, the document and the action.

MessageWhyWhat to do
The item {0} total quantity in from doc is {1} and in current document is {2}The item's total in this unit differs between the two documents, or the item appears on only one of them.Correct the quantities, or add or remove the item, so that the totals match.
No from doc specifiedThe document has no From Document.Pick the From Document, or keep this kind of document out of the flow.

The script ​

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

import com.namasoft.common.Pair
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.entity.base.EntityAction
import com.namasoft.infra.domainbase.entity.generic.GenericReference
import com.namasoft.infra.domainbase.util.Result
import com.namasoft.modules.supplychain.domain.details.BasicSCDocumentLine
import com.namasoft.modules.supplychain.domain.entities.BasicSCDocument
import com.namasoft.modules.supplychain.domain.entities.InvItem
import com.namasoft.modules.supplychain.domain.entities.UOM

public class EAQtyMustMatchFromDocPerItem implements EntityAction<BasicSCDocument> {
    @Override
    Result doAction(BasicSCDocument doc, LongTextDF... parameters) {
        Result result = Result.createAccumulatingResult();
        BasicSCDocument fromDoc = GenericReference.toReal(doc.getFromDoc());
        if (fromDoc == null)
            return Result.createFailureResult("No from doc specified");
        HashMap<Pair<InvItem, UOM>, DecimalDF> docTotals = mapDocLines(doc)
        HashMap<Pair<InvItem, UOM>, DecimalDF> fromDocTotals = mapDocLines(fromDoc)
        Set<Pair<InvItem, UOM>> all = new HashSet<>(docTotals.keySet());
        all.addAll(fromDocTotals.keySet());
        for (final def item in all) {
            if (ObjectChecker.areNotEqual(fromDocTotals.get(item), docTotals.get(item)))
                Result.createFailureResult("The item {0} total quantity in from doc is {1} and in current document is {2}",
                        item.getX(), fromDocTotals.get(item), docTotals.get(item)).addToAccumulatingResult(result);
        }
        return result;
    }

    private HashMap<Pair<InvItem, UOM>, DecimalDF> mapDocLines(BasicSCDocument doc) {
        Map<Pair<InvItem, UOM>, DecimalDF> docTotals = new HashMap<>();
        for (BasicSCDocumentLine l : doc.getDetails()) {
            docTotals.computeIfAbsent(new Pair<>(l.fetchItem(), l.fetchPrimeQtyUOM()), { DecimalDF.zero() }).mutateAdd(l.fetchPrimeQtyValue());
        }
        docTotals
    }

    @Override
    String describe() {
        return "Supply chain documents: refuses the save unless, for every item and unit, the total quantity of the document equals the total quantity of its from-doc. " +
                "Every difference is reported. A document with no from-doc is refused. Parameter 1 is not used.";
    }

    @Override
    List<String> columnNames() {
        return Arrays.asList("Not used - leave empty");
    }
}