diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java index aacbdc8002..5abd425193 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java @@ -34,6 +34,7 @@ import org.eclipse.dirigible.components.intent.model.EntityIntent; import org.eclipse.dirigible.components.intent.model.LabelExpression; import org.eclipse.dirigible.components.intent.model.FieldIntent; +import org.eclipse.dirigible.components.intent.model.NumberIntent; import org.eclipse.dirigible.components.intent.model.IntentModel; import org.eclipse.dirigible.components.intent.model.RelationIntent; import org.eclipse.dirigible.components.intent.model.RollupIntent; @@ -924,6 +925,32 @@ private static Map propertyMap(String entityName, FieldIntent fi if ("uuid".equalsIgnoreCase(field.getType())) { p.put("generatedUuid", "true"); } + // First-class document numbering (intent `number: {}`): the platform maintains a per-series + // counter and stamps the formatted number. stampOn:create stamps the real number on insert + // (the generated repository calls sdk.numbering.DocumentNumbers); stampOn:issue holds a UUID + // placeholder on create (reusing the uuid auto-fill above) until the generated stamp step runs. + if (field.getNumber() != null) { + NumberIntent number = field.getNumber(); + List numberScope = new ArrayList<>(); + if (number.getScope() != null) { + for (String scopeName : number.getScope()) { + // Scope names index the counter AND read the entity's field on create; PascalCase them + // to match the generated entity property (year stays the literal token). + numberScope.add("year".equalsIgnoreCase(scopeName) ? "year" : IntentNaming.pascalCase(scopeName)); + } + } + p.put("numberSeries", number.getSeries() == null ? entityName : number.getSeries()); + p.put("numberFormat", number.getFormat() == null ? "" : number.getFormat()); + p.put("numberScope", numberScope); + p.put("isReadOnlyProperty", "true"); + if ("issue".equalsIgnoreCase(number.getStampOn())) { + p.put("numberStampOn", "issue"); + p.put("generatedUuid", "true"); // UUID placeholder on create; stamped at the issue step + } else { + p.put("numberStampOn", "create"); + p.put("numberStampOnCreate", "true"); // real number allocated + formatted on insert + } + } if (field.isSensitive()) { // Hidden from the personal (my) surface: absent from its pages and stripped from the // personal REST controller's responses. The power surface ignores this attribute. diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java index d9562c2c82..62b3a2c7a4 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java @@ -77,6 +77,39 @@ void customerEmitsCrossModelProjectionsAndForeignKeys() { assertEquals("master-data", customer.get("perspectiveNavId")); } + @Test + void numberFieldEmitsStampMarkers() { + String yaml = + """ + name: billing + entities: + - name: SalesInvoice + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: number, type: string, number: { series: SalesInvoice, format: "SI-{seq:07}", scope: [year], stampOn: issue } } + - name: Proforma + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: number, type: string, number: { series: Proforma, format: "PF-{seq:05}", stampOn: create } } + """; + Map model = EdmIntentGenerator.buildModelJsonForTest(IntentParser.parse(yaml), "billing"); + List> entities = entities(model); + + // stampOn: issue -> a UUID placeholder on create (reusing the uuid auto-fill) + the series markers. + Map siNumber = propertyByName(entityByName(entities, "SalesInvoice"), "Number"); + assertEquals("issue", siNumber.get("numberStampOn")); + assertEquals("SalesInvoice", siNumber.get("numberSeries")); + assertEquals("true", siNumber.get("generatedUuid")); + assertNull(siNumber.get("numberStampOnCreate")); + + // stampOn: create -> the real number is stamped on insert (numberStampOnCreate), no placeholder. + Map pfNumber = propertyByName(entityByName(entities, "Proforma"), "Number"); + assertEquals("create", pfNumber.get("numberStampOn")); + assertEquals("true", pfNumber.get("numberStampOnCreate")); + assertEquals("PF-{seq:05}", pfNumber.get("numberFormat")); + assertNull(pfNumber.get("generatedUuid")); + } + @Test void crossModelProjectionCellIsMarkedProjectionInTheEdmDiagram() { IntentModel parsed = IntentParser.parse(readResource("/billing/customers.intent")); diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template index abffae0ab6..c1f660d89a 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template @@ -144,6 +144,8 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #end #end ## A uuid field (intent type: uuid) is platform-generated: assign a random UUID on create when empty. +## A number: {} field with stampOn:issue also carries generatedUuid, so its create-time placeholder is +## a UUID (the real number is stamped at the issue step); stampOn:create fields are handled just below. #foreach ($property in $properties) #if($property.generatedUuid) if (entity.${property.name} == null || entity.${property.name}.isBlank()) { @@ -151,6 +153,24 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { } #end #end +## First-class numbering, stampOn:create: allocate + format the real document number on insert (when +## empty), via the shared per-tenant counter. The scope both partitions the counter and feeds the +## format's tokens; `year` is the current year, any other name reads the entity's own field. +#foreach ($property in $properties) +#if($property.numberStampOnCreate) + if (entity.${property.name} == null || entity.${property.name}.isBlank()) { + java.util.Map ${property.name}Scope = new java.util.LinkedHashMap<>(); +#foreach ($scopeName in $property.numberScope) +#if($scopeName == "year") + ${property.name}Scope.put("year", String.valueOf(java.time.Year.now().getValue())); +#else + ${property.name}Scope.put("${scopeName}", entity.${scopeName} == null ? "" : String.valueOf(entity.${scopeName})); +#end +#end + entity.${property.name} = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${property.numberSeries}", "${property.numberFormat}", ${property.name}Scope); + } +#end +#end #if($documentMaster) recalculate(entity); #end