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,6 +30,12 @@ dependencies { | ||
| 30 | implementation(project(":platform:platform-security")) // for PermissionEvaluator refresh wiring | 30 | implementation(project(":platform:platform-security")) // for PermissionEvaluator refresh wiring |
| 31 | implementation(project(":platform:platform-workflow")) // for per-plug-in TaskHandler registration | 31 | implementation(project(":platform:platform-workflow")) // for per-plug-in TaskHandler registration |
| 32 | implementation(project(":platform:platform-jobs")) // for per-plug-in JobHandler registration | 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 | implementation(libs.spring.boot.starter) | 40 | implementation(libs.spring.boot.starter) |
| 35 | implementation(libs.spring.boot.starter.web) // for @RestController on the dispatcher | 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,8 +9,10 @@ import org.vibeerp.api.v1.persistence.Transaction | ||
| 9 | import org.vibeerp.api.v1.plugin.PluginContext | 9 | import org.vibeerp.api.v1.plugin.PluginContext |
| 10 | import org.vibeerp.api.v1.plugin.PluginEndpointRegistrar | 10 | import org.vibeerp.api.v1.plugin.PluginEndpointRegistrar |
| 11 | import org.vibeerp.api.v1.plugin.PluginJdbc | 11 | import org.vibeerp.api.v1.plugin.PluginJdbc |
| 12 | +import org.vibeerp.api.v1.files.FileStorage | ||
| 12 | import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar | 13 | import org.vibeerp.api.v1.jobs.PluginJobHandlerRegistrar |
| 13 | import org.vibeerp.api.v1.plugin.PluginLogger | 14 | import org.vibeerp.api.v1.plugin.PluginLogger |
| 15 | +import org.vibeerp.api.v1.reports.ReportRenderer | ||
| 14 | import org.vibeerp.api.v1.security.PermissionCheck | 16 | import org.vibeerp.api.v1.security.PermissionCheck |
| 15 | import org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar | 17 | import org.vibeerp.api.v1.workflow.PluginTaskHandlerRegistrar |
| 16 | 18 | ||
| @@ -51,6 +53,8 @@ internal class DefaultPluginContext( | @@ -51,6 +53,8 @@ internal class DefaultPluginContext( | ||
| 51 | private val sharedLocaleProvider: LocaleProvider, | 53 | private val sharedLocaleProvider: LocaleProvider, |
| 52 | private val scopedTaskHandlers: PluginTaskHandlerRegistrar, | 54 | private val scopedTaskHandlers: PluginTaskHandlerRegistrar, |
| 53 | private val scopedJobHandlers: PluginJobHandlerRegistrar, | 55 | private val scopedJobHandlers: PluginJobHandlerRegistrar, |
| 56 | + private val sharedFileStorage: FileStorage, | ||
| 57 | + private val sharedReportRenderer: ReportRenderer, | ||
| 54 | ) : PluginContext { | 58 | ) : PluginContext { |
| 55 | 59 | ||
| 56 | override val logger: PluginLogger = Slf4jPluginLogger(pluginId, delegateLogger) | 60 | override val logger: PluginLogger = Slf4jPluginLogger(pluginId, delegateLogger) |
| @@ -119,6 +123,24 @@ internal class DefaultPluginContext( | @@ -119,6 +123,24 @@ internal class DefaultPluginContext( | ||
| 119 | */ | 123 | */ |
| 120 | override val jobs: PluginJobHandlerRegistrar = scopedJobHandlers | 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 | // ─── Not yet implemented ─────────────────────────────────────── | 144 | // ─── Not yet implemented ─────────────────────────────────────── |
| 123 | 145 | ||
| 124 | override val transaction: Transaction | 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 +8,10 @@ import org.springframework.beans.factory.InitializingBean | ||
| 8 | import org.springframework.boot.context.properties.ConfigurationProperties | 8 | import org.springframework.boot.context.properties.ConfigurationProperties |
| 9 | import org.springframework.stereotype.Component | 9 | import org.springframework.stereotype.Component |
| 10 | import org.vibeerp.api.v1.event.EventBus | 10 | import org.vibeerp.api.v1.event.EventBus |
| 11 | +import org.vibeerp.api.v1.files.FileStorage | ||
| 11 | import org.vibeerp.api.v1.i18n.LocaleProvider | 12 | import org.vibeerp.api.v1.i18n.LocaleProvider |
| 12 | import org.vibeerp.api.v1.plugin.PluginJdbc | 13 | import org.vibeerp.api.v1.plugin.PluginJdbc |
| 14 | +import org.vibeerp.api.v1.reports.ReportRenderer | ||
| 13 | import org.vibeerp.platform.i18n.IcuTranslator | 15 | import org.vibeerp.platform.i18n.IcuTranslator |
| 14 | import org.vibeerp.platform.metadata.MetadataLoader | 16 | import org.vibeerp.platform.metadata.MetadataLoader |
| 15 | import org.vibeerp.platform.metadata.customfield.CustomFieldRegistry | 17 | import org.vibeerp.platform.metadata.customfield.CustomFieldRegistry |
| @@ -73,6 +75,8 @@ class VibeErpPluginManager( | @@ -73,6 +75,8 @@ class VibeErpPluginManager( | ||
| 73 | private val taskHandlerRegistry: TaskHandlerRegistry, | 75 | private val taskHandlerRegistry: TaskHandlerRegistry, |
| 74 | private val pluginProcessDeployer: PluginProcessDeployer, | 76 | private val pluginProcessDeployer: PluginProcessDeployer, |
| 75 | private val jobHandlerRegistry: JobHandlerRegistry, | 77 | private val jobHandlerRegistry: JobHandlerRegistry, |
| 78 | + private val fileStorage: FileStorage, | ||
| 79 | + private val reportRenderer: ReportRenderer, | ||
| 76 | ) : DefaultPluginManager(Paths.get(properties.directory)), InitializingBean, DisposableBean { | 80 | ) : DefaultPluginManager(Paths.get(properties.directory)), InitializingBean, DisposableBean { |
| 77 | 81 | ||
| 78 | private val log = LoggerFactory.getLogger(VibeErpPluginManager::class.java) | 82 | private val log = LoggerFactory.getLogger(VibeErpPluginManager::class.java) |
| @@ -285,6 +289,8 @@ class VibeErpPluginManager( | @@ -285,6 +289,8 @@ class VibeErpPluginManager( | ||
| 285 | sharedLocaleProvider = localeProvider, | 289 | sharedLocaleProvider = localeProvider, |
| 286 | scopedTaskHandlers = ScopedTaskHandlerRegistrar(taskHandlerRegistry, pluginId), | 290 | scopedTaskHandlers = ScopedTaskHandlerRegistrar(taskHandlerRegistry, pluginId), |
| 287 | scopedJobHandlers = ScopedJobHandlerRegistrar(jobHandlerRegistry, pluginId), | 291 | scopedJobHandlers = ScopedJobHandlerRegistrar(jobHandlerRegistry, pluginId), |
| 292 | + sharedFileStorage = fileStorage, | ||
| 293 | + sharedReportRenderer = reportRenderer, | ||
| 288 | ) | 294 | ) |
| 289 | try { | 295 | try { |
| 290 | vibeErpPlugin.start(context) | 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,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 | // ─── Workflow task handlers (P2.1 plug-in-loaded registration) ── | 357 | // ─── Workflow task handlers (P2.1 plug-in-loaded registration) ── |
| 276 | // The host's VibeErpPluginManager wires context.taskHandlers to a | 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> |