Number Lot-Tracked Lines by Item Section and Year
This Groovy script writes a running number into Text 1 of every document line whose item is lot tracked. The number is built from a fixed prefix, the year of the document, the item's section and a five-digit serial, for example QC-26-RAW00001.
Recommended pattern
This script is not tied to any customer's setup. It works as it is; its only parameter is the prefix.
| Class name | EAAutoLotIdFromItemSectionAndYear |
| Script | EAAutoLotIdFromItemSectionAndYear.groovy |
| Runs on | Any supply chain document with lines, such as a stock receipt or a stock issue. Target action Pre Update Calculated Fields. |
| Uses | Parameter 2 (optional). Leave Parameter 1 empty. |
| Shipped as | Source to paste. It is not built into the system. |
When to use it
A quality team often wants a reference on every incoming batch that tells at a glance which section the material belongs to and in which year it arrived: QC-26-RAW00017 is the 17th raw-material batch of 2026. Typing these numbers by hand leads to gaps and duplicates. This script hands them out on save and keeps a separate count for each section and each year.
The number goes into Text 1 of the line, not into the Lot ID itself. That leaves the lot free for the supplier's own batch number, while Text 1 carries your internal reference.
How it works
- It picks the lines. A line is numbered only when it has an item, the item has Has Lot switched on, and the line's Text 1 is still empty. A line that already has a Text 1 keeps it, so saving the document again never renumbers it.
- It builds the prefix. The prefix is the text in Parameter 2 (or
QC-when Parameter 2 is empty), then the last two digits of the document's value date year, a hyphen, and the code of the item's Item Section. A line whose item has no item section is skipped. - It finds the next serial. It looks at Text 1 of all saved lines of the same kind of document (all stock receipt lines, for example) that start with the same prefix, and takes the highest five-digit serial among them. The line gets the next one. Lines on the same document that share a prefix get consecutive numbers.
- It stops at 99999. If the next serial for a prefix would pass 99999, the line is left without a number and the save is refused with the message below.
The script refuses to run twice at the same time. If two users save at the same moment, the second save is refused and can simply be repeated. This keeps two documents from taking the same number.
Each document type counts on its own
The count is kept per kind of document line. A stock receipt and a stock issue can both have a line numbered QC-26-RAW00001.
Setting it up
- Make sure every lot-tracked item has an Item Section (Inventory → Master Files → Item Section). Its code becomes part of the number, so keep section codes short.
- Open Administration → Display Customization → Entity Flow and create a flow whose target type is the document to number (for example Stock Receipt).
- In the Details grid, add a line with Target Action set to Pre Update Calculated Fields, and paste the whole script into its Groovy Script column. Leave Class Name empty: it is filled in when you save.
- Fill in the parameters and save the flow.
| Parameter | Title shown after saving | What to enter |
|---|---|---|
| Parameter 1 | Not used - leave empty | Nothing. |
| Parameter 2 | Static prefix, defaults to QC- when left empty | The fixed start of every number, for example QC- or LAB/. |
Example
The item section RAW holds raw materials, and Parameter 2 is empty. A stock receipt dated in 2026 has two lot-tracked raw-material lines and one line for an item without lots. When it is saved:
- the first line gets Text 1
QC-26-RAW00001, - the second line gets
QC-26-RAW00002, - the third line is left alone.
The next stock receipt of 2026 continues with QC-26-RAW00003. In January 2027 the count restarts at QC-27-RAW00001.
Messages you may see
| Message | Why | What to do |
|---|---|---|
| The serial of {0} reached its maximum {1} | The prefix {0} has already used every serial up to {1} (99999). This message has no Arabic translation, so it appears in English on Arabic screens. | Change the prefix in Parameter 2 so a new count starts. |
| You can not run the entity action class {0} multiple times — «لا يمكنك تنفيذ الإجراء {0} أكثر من مرة في نفس الوقت» | Another save was running the script at the same moment. {0} is the script's class name. | Save again. |
The script
package com.namasoft.modules.supplychain.domain.utils.plugnplay.groovy
import com.namasoft.common.utilities.ObjectChecker
import com.namasoft.common.utilities.StringUtils
import com.namasoft.infra.domainbase.datafields.BooleanDF
import com.namasoft.infra.domainbase.datafields.DateDF
import com.namasoft.infra.domainbase.datafields.LongTextDF
import com.namasoft.infra.domainbase.datafields.TextDF
import com.namasoft.infra.domainbase.entity.base.EntityAction
import com.namasoft.infra.domainbase.persistence.repos.Persister
import com.namasoft.infra.domainbase.util.Result
import com.namasoft.modules.basic.domain.primitives.EntityTargetAction
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.ItemSection
public class EAAutoLotIdFromItemSectionAndYear implements EntityAction<BasicSCDocument> {
private static final String DEFAULT_STATIC_PREFIX = "QC-";
private static final int STATIC_PREFIX_PARAM_INDEX = 1;
private static final int YEAR_LENGTH = 2;
private static final int SERIAL_LENGTH = 5;
private static final long MAX_SERIAL = 99999L;
@Override
Result doAction(EntityTargetAction action, BasicSCDocument doc, LongTextDF... parameters) {
Result result = Result.createAccumulatingResult();
List<BasicSCDocumentLine> details = doc.getDetails();
if (ObjectChecker.isEmptyOrNull(details))
return result;
String staticPrefix = fetchStaticPrefix(parameters);
String yearPart = calcYearPart(doc);
String lineTableName = details.getFirst().getClass().getSimpleName();
Map<String, Long> lastSerialPerPrefix = new HashMap<>();
for (BasicSCDocumentLine line : details) {
if (!needsGeneratedLot(line))
continue;
String sectionCode = fetchSectionCode(line.fetchItem());
if (ObjectChecker.isEmptyOrNull(sectionCode))
continue;
String prefix = staticPrefix + yearPart + "-" + sectionCode;
Long serial = nextSerial(prefix, lineTableName, lastSerialPerPrefix, result);
if (serial == null)
continue;
String generatedCode = prefix + StringUtils.PadLeft(serial.toString(), SERIAL_LENGTH);
line.setText1(TextDF.fromString(generatedCode));
}
return result;
}
private static String fetchStaticPrefix(LongTextDF[] parameters) {
if (parameters == null || parameters.length <= STATIC_PREFIX_PARAM_INDEX)
return DEFAULT_STATIC_PREFIX;
if (ObjectChecker.isEmptyOrNull(parameters[STATIC_PREFIX_PARAM_INDEX]))
return DEFAULT_STATIC_PREFIX;
String staticPrefix = parameters[STATIC_PREFIX_PARAM_INDEX].getPrimitiveValue().trim();
return ObjectChecker.isEmptyOrNull(staticPrefix) ? DEFAULT_STATIC_PREFIX : staticPrefix;
}
private static boolean needsGeneratedLot(BasicSCDocumentLine line) {
InvItem item = line.fetchItem();
if (item == null)
return false;
if (BooleanDF.isFalse(item.getHasLot()))
return false;
return ObjectChecker.isEmptyOrNull(line.getText1());
}
private static String fetchSectionCode(InvItem item) {
ItemSection section = item.getSection();
if (section == null || section.getCode() == null)
return null;
return ObjectChecker.toStringOrEmpty(section.getCode().getPrimitiveValue()).trim();
}
private static String calcYearPart(BasicSCDocument doc) {
DateDF valueDate = doc.getValueDate();
if (ObjectChecker.isEmptyOrNull(valueDate))
valueDate = DateDF.today();
return StringUtils.PadLeft(String.valueOf(valueDate.getYear() % 100), YEAR_LENGTH);
}
private static Long nextSerial(String prefix, String lineTableName, Map<String, Long> lastSerialPerPrefix, Result result) {
Long lastSerial = lastSerialPerPrefix.get(prefix);
if (lastSerial == null)
lastSerial = selectLastUsedSerial(prefix, lineTableName);
long serial = lastSerial + 1;
if (serial > MAX_SERIAL) {
Result.createFailureResult("The serial of {0} reached its maximum {1}", prefix, MAX_SERIAL).addToAccumulatingResult(result);
return null;
}
lastSerialPerPrefix.put(prefix, serial);
return serial;
}
private static long selectLastUsedSerial(String prefix, String lineTableName) {
Map<String, Object> queryParameters = new HashMap<>();
queryParameters.put("pattern", prefix + "%");
List<Object[]> rows = Persister.prepareAndExecuteJDBCQuery(queryParameters,
'select text1 from ' + lineTableName + ' where text1 like {pattern}');
long lastSerial = 0;
for (Object[] row : rows) {
Long serial = extractSerial(prefix, ObjectChecker.toStringOrEmpty(row[0]));
if (serial != null && serial > lastSerial)
lastSerial = serial;
}
return lastSerial;
}
private static Long extractSerial(String prefix, String text1) {
String code = text1.trim();
if (code.length() != prefix.length() + SERIAL_LENGTH)
return null;
if (!code.toUpperCase().startsWith(prefix.toUpperCase()))
return null;
String serial = code.substring(prefix.length());
if (!(serial ==~ /\d+/))
return null;
return ObjectChecker.tryParseLong(serial);
}
@Override
boolean shouldPreventSimultaneousRuns() {
return true;
}
@Override
List<String> columnNames() {
return Arrays.asList("Not used - leave empty", "Static prefix, defaults to QC- when left empty");
}
@Override
String describe() {
return "Fills Text 1 of every document line whose item is lot tracked and whose Text 1 is still empty with " +
"prefix + 2-digit year + - + item section code + 5-digit serial, for example QC-26-RAW00001. " +
"Parameter 2 is the prefix (QC- when empty). The year comes from the document's value date. " +
"The serial continues after the highest one already saved in Text 1 of the same kind of document line, separately for each " +
"prefix, year and section. Lines whose item has no item section are skipped. Two runs can not happen at the same time. " +
"Parameter 1 is not used.";
}
}Related
- EAGroovyAction: how Groovy scripts are attached to entity flows.
- Introduction to Entity Flows: what each target action means.
- Understanding Inventory Items: lot tracking on the item.