Commit e4f7cf42661e4e5963b5a7224e53d3c49ac0568c
1 parent
d243a9a4
feat(plugins+ref-plugin): wire FileStorage + ReportRenderer through PluginContext + quote PDF demo
Closes two open wiring gaps left by the P1.9 and P1.8 chunks —
`PluginContext.files` and `PluginContext.reports` both previously
threw `UnsupportedOperationException` because the host's
`DefaultPluginContext` never received the concrete beans. This
commit plumbs both through and exercises them end-to-end via a
new printing-shop plug-in endpoint that generates a quote PDF,
stores it in the file store, and returns the file handle.
With this chunk the reference printing-shop plug-in demonstrates
**every extension seam the framework provides**: HTTP endpoints,
JDBC, metadata YAML, i18n, BPMN + TaskHandlers, JobHandlers,
custom fields on core entities, event publishing via EventBus,
ReportRenderer, and FileStorage. There is no major public plug-in
surface left unexercised.
## Wiring: DefaultPluginContext + VibeErpPluginManager
- `DefaultPluginContext` gains two new constructor parameters
(`sharedFileStorage: FileStorage`, `sharedReportRenderer: ReportRenderer`)
and two new overrides. Each is wired via Spring — they live in
platform-files and platform-reports respectively, but
platform-plugins only depends on api.v1 (the interfaces) and
NOT on those modules directly. The concrete beans are injected
by Spring at distribution boot time when every `@Component` is
on the classpath.
- `VibeErpPluginManager` adds `private val fileStorage: FileStorage`
and `private val reportRenderer: ReportRenderer` constructor
params and passes them through to every `DefaultPluginContext`
it builds per plug-in.
The `files` and `reports` getters in api.v1 `PluginContext` still
have their default-throw backward-compat shim — a plug-in built
against v0.8 of api.v1 loading on a v0.7 host would still fail
loudly at first call with a clear "upgrade to v0.8" message. The
override here makes the v0.8+ host honour the interface.
## Printing-shop reference — quote PDF endpoint
- New `resources/reports/quote-template.jrxml` inside the plug-in
JAR. Parameters: plateCode, plateName, widthMm, heightMm,
status, customerName. Produces a single-page A4 PDF with a
header, a table of plate attributes, and a footer.
- New endpoint `POST /api/v1/plugins/printing-shop/plates/{id}/generate-quote-pdf`.
Request body `{"customerName": "..."}`, response:
`{"plateId", "plateCode", "customerName",
"fileKey", "fileSize", "fileContentType", "downloadUrl"}`
The handler does ALL of:
1. Reads the plate row via `context.jdbc.queryForObject(...)`
2. Loads the JRXML from the PLUG-IN's own classloader (not
the host classpath — `this::class.java.classLoader
.getResourceAsStream("reports/quote-template.jrxml")` —
so the host's built-in `vibeerp-ping-report.jrxml` and the
plug-in's template live in isolated namespaces)
3. Renders via `context.reports.renderPdf(template, data)`
— uses the host JasperReportRenderer under the hood
4. Persists via `context.files.put(key, contentType, content)`
under a plug-in-scoped key `plugin-printing-shop/quotes/quote-<code>.pdf`
5. Returns the file handle plus a `downloadUrl` pointing at
the framework's `/api/v1/files/download` endpoint the
caller can immediately hit
## Smoke test (fresh DB + staged plug-in)
```
# create a plate
POST /api/v1/plugins/printing-shop/plates
{code: PLATE-200, name: "Premium cover", widthMm: 420, heightMm: 594}
→ 201 {id, code: PLATE-200, status: DRAFT, ...}
# generate + store the quote PDF
POST /api/v1/plugins/printing-shop/plates/<id>/generate-quote-pdf
{customerName: "Acme Inc"}
→ 201 {
plateId, plateCode: "PLATE-200", customerName: "Acme Inc",
fileKey: "plugin-printing-shop/quotes/quote-PLATE-200.pdf",
fileSize: 1488,
fileContentType: "application/pdf",
downloadUrl: "/api/v1/files/download?key=plugin-printing-shop/quotes/quote-PLATE-200.pdf"
}
# download via the framework's file endpoint
GET /api/v1/files/download?key=plugin-printing-shop/quotes/quote-PLATE-200.pdf
→ 200
Content-Type: application/pdf
Content-Length: 1488
body: valid PDF 1.5, 1 page
$ file /tmp/plate-quote.pdf
/tmp/plate-quote.pdf: PDF document, version 1.5, 1 pages (zip deflate encoded)
# list by prefix
GET /api/v1/files?prefix=plugin-printing-shop/
→ [{"key":"plugin-printing-shop/quotes/quote-PLATE-200.pdf",
"size":1488, "contentType":"application/pdf", ...}]
# plug-in log
[plugin:printing-shop] registered 8 endpoints under /api/v1/plugins/printing-shop/
[plugin:printing-shop] generated quote PDF for plate PLATE-200 (1488 bytes)
→ plugin-printing-shop/quotes/quote-PLATE-200.pdf
```
Four public surfaces composed in one flow: plug-in JDBC read →
plug-in classloader resource load → host ReportRenderer compile/
fill/export → host FileStorage put → host file controller
download. Every step stays on api.v1; zero plug-in code reaches
into a concrete platform class.
## Printing-shop plug-in — full extension surface exercised
After this commit the reference printing-shop plug-in contributes
via every public seam the framework offers:
| Seam | How the plug-in uses it |
|-------------------------------|--------------------------------------------------------|
| HTTP endpoints (P1.3) | 8 endpoints under /api/v1/plugins/printing-shop/ |
| JDBC (P1.4) | Reads/writes its own plugin_printingshop__* tables |
| Liquibase | Own changelog.xml, 2 tables created at plug-in start |
| Metadata YAML (P1.5) | 2 entities, 5 permissions, 2 menus |
| Custom fields on CORE (P3.4) | 5 plug-in fields on Partner/Item/SalesOrder/WorkOrder |
| i18n (P1.6) | Own messages_<locale>.properties, quote number msgs |
| EventBus (P1.7) | Publishes WorkOrderRequestedEvent from a TaskHandler |
| TaskHandlers (P2.1) | 2 handlers (plate-approval, quote-to-work-order) |
| Plug-in BPMN (P2.1 followup) | 2 BPMNs in processes/ auto-deployed at start |
| JobHandlers (P1.10 followup) | PlateCleanupJobHandler using context.jdbc + logger |
| ReportRenderer (P1.8) | Quote PDF from JRXML via context.reports |
| FileStorage (P1.9) | Persists quote PDF via context.files |
Everything listed in this table is exercised end-to-end by the
current smoke test. The plug-in is the framework's executable
acceptance test for the entire public extension surface.
## Tests
No new unit tests — the wiring change is a plain constructor
addition, the existing `DefaultPluginContext` has no dedicated
test class (it's a thin dataclass-shaped bean), and
`JasperReportRenderer` + `LocalDiskFileStorage` each have their
own unit tests from the respective parent chunks. The change is
validated end-to-end by the above smoke test; formalizing that
into an integration test would need Testcontainers + a real
plug-in JAR and belongs to a different (test-infra) chunk.
- Total framework unit tests: 337 (unchanged), all green.
## Non-goals (parking lot)
- Pre-compiled `.jasper` caching keyed by template hash. A
hot-path benchmark would tell us whether the cache is worth
shipping.
- Multipart upload of a template into a plug-in's own `files`
namespace so non-bundled templates can be tried without a
plug-in rebuild. Nice-to-have for iteration but not on the
v1.0 critical path.
- Scoped file-key prefixes per plug-in enforced by the framework
(today the plug-in picks its own prefix by convention; a
`plugin.files.keyPrefix` config would let the host enforce
that every plug-in-contributed file lives under
`plugin-<id>/`). Future hardening chunk.
Showing
5 changed files
with
238 additions
and
1 deletions
platform/platform-plugins/build.gradle.kts
| ... | ... | @@ -30,6 +30,12 @@ dependencies { |
| 30 | 30 | implementation(project(":platform:platform-security")) // for PermissionEvaluator refresh wiring |
| 31 | 31 | implementation(project(":platform:platform-workflow")) // for per-plug-in TaskHandler registration |
| 32 | 32 | implementation(project(":platform:platform-jobs")) // for per-plug-in JobHandler registration |
| 33 | + // Note: platform-files and platform-reports are NOT needed here | |
| 34 | + // because DefaultPluginContext accepts api.v1 FileStorage + | |
| 35 | + // ReportRenderer interfaces (both in api-v1), and the concrete | |
| 36 | + // beans are wired by Spring at distribution boot. Adding them | |
| 37 | + // as dependencies would work too but would unnecessarily couple | |
| 38 | + // platform-plugins to two more modules. | |
| 33 | 39 | |
| 34 | 40 | implementation(libs.spring.boot.starter) |
| 35 | 41 | implementation(libs.spring.boot.starter.web) // for @RestController on the dispatcher | ... | ... |
platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/DefaultPluginContext.kt
| ... | ... | @@ -9,8 +9,10 @@ import org.vibeerp.api.v1.persistence.Transaction |
| 9 | 9 | import org.vibeerp.api.v1.plugin.PluginContext |
| 10 | 10 | import org.vibeerp.api.v1.plugin.PluginEndpointRegistrar |
| 11 | 11 | import org.vibeerp.api.v1.plugin.PluginJdbc |
| 12 | +import org.vibeerp.api.v1.files.FileStorage | |
| 12 | 13 | import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar |
| 13 | 14 | import org.vibeerp.api.v1.plugin.PluginLogger |
| 15 | +import org.vibeerp.api.v1.reports.ReportRenderer | |
| 14 | 16 | import org.vibeerp.api.v1.security.PermissionCheck |
| 15 | 17 | import org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar |
| 16 | 18 | |
| ... | ... | @@ -51,6 +53,8 @@ internal class DefaultPluginContext( |
| 51 | 53 | private val sharedLocaleProvider: LocaleProvider, |
| 52 | 54 | private val scopedTaskHandlers: PluginTaskHandlerRegistrar, |
| 53 | 55 | private val scopedJobHandlers: PluginJobHandlerRegistrar, |
| 56 | + private val sharedFileStorage: FileStorage, | |
| 57 | + private val sharedReportRenderer: ReportRenderer, | |
| 54 | 58 | ) : PluginContext { |
| 55 | 59 | |
| 56 | 60 | override val logger: PluginLogger = Slf4jPluginLogger(pluginId, delegateLogger) |
| ... | ... | @@ -119,6 +123,24 @@ internal class DefaultPluginContext( |
| 119 | 123 | */ |
| 120 | 124 | override val jobs: PluginJobHandlerRegistrar = scopedJobHandlers |
| 121 | 125 | |
| 126 | + /** | |
| 127 | + * Shared host [FileStorage]. Plug-ins persist generated | |
| 128 | + * report PDFs, uploaded attachments, exports, and any other | |
| 129 | + * binary blobs through this. Wired in P1.9 + the plug-in | |
| 130 | + * loader follow-up; backed by `platform-files`' | |
| 131 | + * [org.vibeerp.platform.files.LocalDiskFileStorage] by default. | |
| 132 | + */ | |
| 133 | + override val files: FileStorage = sharedFileStorage | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * Shared host [ReportRenderer]. Plug-ins render JRXML | |
| 137 | + * templates (loaded from their own classpath, the file store, | |
| 138 | + * or a database row) into PDF bytes. Wired alongside | |
| 139 | + * [files] in the same chunk so a plug-in can do | |
| 140 | + * "render → store → return handle" in one method. | |
| 141 | + */ | |
| 142 | + override val reports: ReportRenderer = sharedReportRenderer | |
| 143 | + | |
| 122 | 144 | // ─── Not yet implemented ─────────────────────────────────────── |
| 123 | 145 | |
| 124 | 146 | override val transaction: Transaction | ... | ... |
platform/platform-plugins/src/main/kotlin/org/vibeerp/platform/plugins/VibeErpPluginManager.kt
| ... | ... | @@ -8,8 +8,10 @@ import org.springframework.beans.factory.InitializingBean |
| 8 | 8 | import org.springframework.boot.context.properties.ConfigurationProperties |
| 9 | 9 | import org.springframework.stereotype.Component |
| 10 | 10 | import org.vibeerp.api.v1.event.EventBus |
| 11 | +import org.vibeerp.api.v1.files.FileStorage | |
| 11 | 12 | import org.vibeerp.api.v1.i18n.LocaleProvider |
| 12 | 13 | import org.vibeerp.api.v1.plugin.PluginJdbc |
| 14 | +import org.vibeerp.api.v1.reports.ReportRenderer | |
| 13 | 15 | import org.vibeerp.platform.i18n.IcuTranslator |
| 14 | 16 | import org.vibeerp.platform.metadata.MetadataLoader |
| 15 | 17 | import org.vibeerp.platform.metadata.customfield.CustomFieldRegistry |
| ... | ... | @@ -73,6 +75,8 @@ class VibeErpPluginManager( |
| 73 | 75 | private val taskHandlerRegistry: TaskHandlerRegistry, |
| 74 | 76 | private val pluginProcessDeployer: PluginProcessDeployer, |
| 75 | 77 | private val jobHandlerRegistry: JobHandlerRegistry, |
| 78 | + private val fileStorage: FileStorage, | |
| 79 | + private val reportRenderer: ReportRenderer, | |
| 76 | 80 | ) : DefaultPluginManager(Paths.get(properties.directory)), InitializingBean, DisposableBean { |
| 77 | 81 | |
| 78 | 82 | private val log = LoggerFactory.getLogger(VibeErpPluginManager::class.java) |
| ... | ... | @@ -285,6 +289,8 @@ class VibeErpPluginManager( |
| 285 | 289 | sharedLocaleProvider = localeProvider, |
| 286 | 290 | scopedTaskHandlers = ScopedTaskHandlerRegistrar(taskHandlerRegistry, pluginId), |
| 287 | 291 | scopedJobHandlers = ScopedJobHandlerRegistrar(jobHandlerRegistry, pluginId), |
| 292 | + sharedFileStorage = fileStorage, | |
| 293 | + sharedReportRenderer = reportRenderer, | |
| 288 | 294 | ) |
| 289 | 295 | try { |
| 290 | 296 | vibeErpPlugin.start(context) | ... | ... |
reference-customer/plugin-printing-shop/src/main/kotlin/org/vibeerp/reference/printingshop/PrintingShopPlugin.kt
| ... | ... | @@ -270,7 +270,89 @@ class PrintingShopPlugin(wrapper: PluginWrapper) : Pf4jPlugin(wrapper), VibeErpP |
| 270 | 270 | ) |
| 271 | 271 | } |
| 272 | 272 | |
| 273 | - context.logger.info("registered 7 endpoints under /api/v1/plugins/printing-shop/") | |
| 273 | + // ─── POST /plates/{id}/generate-quote-pdf ───────────────── | |
| 274 | + // | |
| 275 | + // Renders a quote PDF for a plate by composing every | |
| 276 | + // extension seam the framework offers: context.jdbc reads | |
| 277 | + // the plate from the plug-in's own table, context.reports | |
| 278 | + // compiles + fills a JRXML template loaded from the | |
| 279 | + // plug-in's OWN classloader (not the host classpath, so | |
| 280 | + // Jasper's classpath auto-deploy doesn't see it), and | |
| 281 | + // context.files persists the PDF under a plug-in-scoped | |
| 282 | + // key so a future GET /api/v1/files/download?key=... can | |
| 283 | + // retrieve it. The response returns the file handle so the | |
| 284 | + // caller can download it immediately. | |
| 285 | + context.endpoints.register(HttpMethod.POST, "/plates/{id}/generate-quote-pdf") { request -> | |
| 286 | + val rawId = request.pathParameters["id"] ?: return@register PluginResponse(400, mapOf("detail" to "missing id")) | |
| 287 | + val id = try { UUID.fromString(rawId) } catch (ex: IllegalArgumentException) { | |
| 288 | + return@register PluginResponse(400, mapOf("detail" to "id is not a valid UUID")) | |
| 289 | + } | |
| 290 | + val body = parseJsonObject(request.body.orEmpty()) | |
| 291 | + val customerName = body["customerName"] as? String ?: "<unknown customer>" | |
| 292 | + | |
| 293 | + val plate = context.jdbc.queryForObject( | |
| 294 | + "SELECT code, name, width_mm, height_mm, status FROM plugin_printingshop__plate WHERE id = :id", | |
| 295 | + mapOf("id" to id), | |
| 296 | + ) { row -> | |
| 297 | + mapOf( | |
| 298 | + "code" to row.string("code"), | |
| 299 | + "name" to row.string("name"), | |
| 300 | + "widthMm" to row.int("width_mm"), | |
| 301 | + "heightMm" to row.int("height_mm"), | |
| 302 | + "status" to row.string("status"), | |
| 303 | + ) | |
| 304 | + } ?: return@register PluginResponse(404, mapOf("detail" to "plate not found: $id")) | |
| 305 | + | |
| 306 | + // Load the JRXML from THIS plug-in's classloader, not | |
| 307 | + // the host's. PF4J's PluginClassLoader is parent-first | |
| 308 | + // for anything not in the plug-in jar, so the plug-in's | |
| 309 | + // own resources/reports/quote-template.jrxml is reached | |
| 310 | + // via the local classloader; the host's ping-report is | |
| 311 | + // invisible to the plug-in, and vice versa. | |
| 312 | + val templateStream = this::class.java.classLoader | |
| 313 | + .getResourceAsStream("reports/quote-template.jrxml") | |
| 314 | + ?: return@register PluginResponse(500, mapOf("detail" to "quote template missing from plug-in classpath")) | |
| 315 | + | |
| 316 | + val pdfBytes = templateStream.use { stream -> | |
| 317 | + context.reports.renderPdf( | |
| 318 | + template = stream, | |
| 319 | + data = mapOf( | |
| 320 | + "plateCode" to (plate["code"] as String), | |
| 321 | + "plateName" to (plate["name"] as String), | |
| 322 | + "widthMm" to (plate["widthMm"] as Int), | |
| 323 | + "heightMm" to (plate["heightMm"] as Int), | |
| 324 | + "status" to (plate["status"] as String), | |
| 325 | + "customerName" to customerName, | |
| 326 | + ), | |
| 327 | + ) | |
| 328 | + } | |
| 329 | + | |
| 330 | + val fileKey = "plugin-printing-shop/quotes/quote-${plate["code"]}.pdf" | |
| 331 | + val handle = context.files.put( | |
| 332 | + key = fileKey, | |
| 333 | + contentType = "application/pdf", | |
| 334 | + content = java.io.ByteArrayInputStream(pdfBytes), | |
| 335 | + ) | |
| 336 | + | |
| 337 | + context.logger.info( | |
| 338 | + "generated quote PDF for plate ${plate["code"]} (${pdfBytes.size} bytes) → $fileKey", | |
| 339 | + ) | |
| 340 | + | |
| 341 | + PluginResponse( | |
| 342 | + status = 201, | |
| 343 | + body = mapOf( | |
| 344 | + "plateId" to id.toString(), | |
| 345 | + "plateCode" to plate["code"], | |
| 346 | + "customerName" to customerName, | |
| 347 | + "fileKey" to handle.key, | |
| 348 | + "fileSize" to handle.size, | |
| 349 | + "fileContentType" to handle.contentType, | |
| 350 | + "downloadUrl" to "/api/v1/files/download?key=${handle.key}", | |
| 351 | + ), | |
| 352 | + ) | |
| 353 | + } | |
| 354 | + | |
| 355 | + context.logger.info("registered 8 endpoints under /api/v1/plugins/printing-shop/") | |
| 274 | 356 | |
| 275 | 357 | // ─── Workflow task handlers (P2.1 plug-in-loaded registration) ── |
| 276 | 358 | // The host's VibeErpPluginManager wires context.taskHandlers to a | ... | ... |
reference-customer/plugin-printing-shop/src/main/resources/reports/quote-template.jrxml
0 → 100644
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!-- | |
| 3 | + Printing-shop plate quote PDF template. | |
| 4 | + | |
| 5 | + Lives inside the plug-in JAR under resources/reports/ and is | |
| 6 | + loaded by PrintingShopPlugin via the plug-in's own classloader | |
| 7 | + (not on the host classpath, so Jasper's default classpath auto- | |
| 8 | + deploy doesn't see it — the plug-in loads the bytes manually and | |
| 9 | + hands them to context.reports.renderPdf). | |
| 10 | + | |
| 11 | + Parameters: | |
| 12 | + plateCode (String) plate's business code | |
| 13 | + plateName (String) human name | |
| 14 | + widthMm (Integer) plate width in mm | |
| 15 | + heightMm (Integer) plate height in mm | |
| 16 | + status (String) current status (DRAFT / APPROVED / ...) | |
| 17 | + customerName (String) customer the quote is for (free-form) | |
| 18 | +--> | |
| 19 | +<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" | |
| 20 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| 21 | + xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" | |
| 22 | + name="printing-shop-quote" | |
| 23 | + pageWidth="595" | |
| 24 | + pageHeight="842" | |
| 25 | + columnWidth="535" | |
| 26 | + leftMargin="30" | |
| 27 | + rightMargin="30" | |
| 28 | + topMargin="40" | |
| 29 | + bottomMargin="40"> | |
| 30 | + <parameter name="plateCode" class="java.lang.String"> | |
| 31 | + <defaultValueExpression><![CDATA["?"]]></defaultValueExpression> | |
| 32 | + </parameter> | |
| 33 | + <parameter name="plateName" class="java.lang.String"> | |
| 34 | + <defaultValueExpression><![CDATA["?"]]></defaultValueExpression> | |
| 35 | + </parameter> | |
| 36 | + <parameter name="widthMm" class="java.lang.Integer"> | |
| 37 | + <defaultValueExpression><![CDATA[0]]></defaultValueExpression> | |
| 38 | + </parameter> | |
| 39 | + <parameter name="heightMm" class="java.lang.Integer"> | |
| 40 | + <defaultValueExpression><![CDATA[0]]></defaultValueExpression> | |
| 41 | + </parameter> | |
| 42 | + <parameter name="status" class="java.lang.String"> | |
| 43 | + <defaultValueExpression><![CDATA["?"]]></defaultValueExpression> | |
| 44 | + </parameter> | |
| 45 | + <parameter name="customerName" class="java.lang.String"> | |
| 46 | + <defaultValueExpression><![CDATA["?"]]></defaultValueExpression> | |
| 47 | + </parameter> | |
| 48 | + <title> | |
| 49 | + <band height="80"> | |
| 50 | + <staticText> | |
| 51 | + <reportElement x="0" y="0" width="535" height="36"/> | |
| 52 | + <textElement textAlignment="Center"> | |
| 53 | + <font size="24" isBold="true"/> | |
| 54 | + </textElement> | |
| 55 | + <text><![CDATA[Printing shop — plate quote]]></text> | |
| 56 | + </staticText> | |
| 57 | + <textField> | |
| 58 | + <reportElement x="0" y="44" width="535" height="20"/> | |
| 59 | + <textElement textAlignment="Center"> | |
| 60 | + <font size="12"/> | |
| 61 | + </textElement> | |
| 62 | + <textFieldExpression><![CDATA["Prepared for " + $P{customerName}]]></textFieldExpression> | |
| 63 | + </textField> | |
| 64 | + </band> | |
| 65 | + </title> | |
| 66 | + <detail> | |
| 67 | + <band height="200"> | |
| 68 | + <staticText> | |
| 69 | + <reportElement x="0" y="20" width="200" height="20"/> | |
| 70 | + <textElement><font size="12" isBold="true"/></textElement> | |
| 71 | + <text><![CDATA[Plate code:]]></text> | |
| 72 | + </staticText> | |
| 73 | + <textField> | |
| 74 | + <reportElement x="210" y="20" width="325" height="20"/> | |
| 75 | + <textElement><font size="12"/></textElement> | |
| 76 | + <textFieldExpression><![CDATA[$P{plateCode}]]></textFieldExpression> | |
| 77 | + </textField> | |
| 78 | + | |
| 79 | + <staticText> | |
| 80 | + <reportElement x="0" y="50" width="200" height="20"/> | |
| 81 | + <textElement><font size="12" isBold="true"/></textElement> | |
| 82 | + <text><![CDATA[Plate name:]]></text> | |
| 83 | + </staticText> | |
| 84 | + <textField> | |
| 85 | + <reportElement x="210" y="50" width="325" height="20"/> | |
| 86 | + <textElement><font size="12"/></textElement> | |
| 87 | + <textFieldExpression><![CDATA[$P{plateName}]]></textFieldExpression> | |
| 88 | + </textField> | |
| 89 | + | |
| 90 | + <staticText> | |
| 91 | + <reportElement x="0" y="80" width="200" height="20"/> | |
| 92 | + <textElement><font size="12" isBold="true"/></textElement> | |
| 93 | + <text><![CDATA[Dimensions:]]></text> | |
| 94 | + </staticText> | |
| 95 | + <textField> | |
| 96 | + <reportElement x="210" y="80" width="325" height="20"/> | |
| 97 | + <textElement><font size="12"/></textElement> | |
| 98 | + <textFieldExpression><![CDATA[$P{widthMm} + " mm × " + $P{heightMm} + " mm"]]></textFieldExpression> | |
| 99 | + </textField> | |
| 100 | + | |
| 101 | + <staticText> | |
| 102 | + <reportElement x="0" y="110" width="200" height="20"/> | |
| 103 | + <textElement><font size="12" isBold="true"/></textElement> | |
| 104 | + <text><![CDATA[Status:]]></text> | |
| 105 | + </staticText> | |
| 106 | + <textField> | |
| 107 | + <reportElement x="210" y="110" width="325" height="20"/> | |
| 108 | + <textElement><font size="12"/></textElement> | |
| 109 | + <textFieldExpression><![CDATA[$P{status}]]></textFieldExpression> | |
| 110 | + </textField> | |
| 111 | + | |
| 112 | + <staticText> | |
| 113 | + <reportElement x="0" y="160" width="535" height="24"/> | |
| 114 | + <textElement textAlignment="Center"> | |
| 115 | + <font size="10" isItalic="true"/> | |
| 116 | + </textElement> | |
| 117 | + <text><![CDATA[Generated by the vibe_erp printing-shop reference plug-in.]]></text> | |
| 118 | + </staticText> | |
| 119 | + </band> | |
| 120 | + </detail> | |
| 121 | +</jasperReport> | ... | ... |