diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a68c1799..86f5d9674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ follow semantic versioning; release dates are ISO 8601. ### Templates +- **A second invoice preset: `ClassicInvoice`.** The letterhead-style invoice — a header + band with the company name and a 28pt INVOICE title, a TOTAL DUE hero strip, + BILL TO / FROM party columns, and a dedicated Summary table composed after the + line items (subtotal / tax / TOTAL, the last row emphasized) — now ships as + `invoice.presets.ClassicInvoice` on the layered stack, with the same + `create()` / `create(BrandTheme)` contract as `ModernInvoice`, porting the rendered + layout of the published standalone `invoice-classic` template. Guarded by a smoke + test, exact layout snapshots (the canonical single page plus a forty-line-item + overflow that freezes the two-page table continuation), and the invoice pixel-parity + gate; the examples showcase gains `invoice-classic-v2`. + - **Monogram Sidebar draws the employer.** Its experience entries rendered the position, the date and the description, and never `CvEntry.subtitle()` — so every company name was missing from the rendered CV while the education block, which does render its diff --git a/assets/readme/examples/invoice-classic-v2.pdf b/assets/readme/examples/invoice-classic-v2.pdf new file mode 100644 index 000000000..1a4c2dc20 Binary files /dev/null and b/assets/readme/examples/invoice-classic-v2.pdf differ diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index 59590ed29..006ec548a 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -96,6 +96,7 @@ import com.demcha.examples.templates.cv.v2.CvPanelExample; import com.demcha.examples.templates.cv.v2.CvSidebarPortraitExample; import com.demcha.examples.templates.cv.v2.CvTimelineMinimalExample; +import com.demcha.examples.templates.invoice.ClassicInvoiceV2Example; import com.demcha.examples.templates.invoice.InvoiceCinematicFileExample; import com.demcha.examples.templates.invoice.ModernInvoiceV2Example; import com.demcha.examples.templates.proposal.CinematicProposalFileExample; @@ -162,6 +163,7 @@ public static void main(String[] args) throws Exception { // Invoices System.out.println("Generated: " + InvoiceCinematicFileExample.generate()); System.out.println("Generated: " + ModernInvoiceV2Example.generate()); + System.out.println("Generated: " + ClassicInvoiceV2Example.generate()); // Proposals System.out.println("Generated: " + ProposalCinematicFileExample.generate()); diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java index deb4e65f9..0f613f678 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -88,6 +88,7 @@ record Entry(String title, String description, List tags, String codeUrl // ===== Templates / Invoice ===== invoice("invoice-cinematic", "InvoiceCinematicFileExample", "Cinematic Invoice", "Layered ModernInvoice preset with theme-driven layout, advanced tables, and totals.", "invoice", "cinematic"); invoice("invoice-modern-v2", "v2/ModernInvoiceV2Example", "Modern Invoice", "The ModernInvoice preset composed straight from an InvoiceDocumentSpec — line items, totals and payment block driven by the BrandTheme rather than per-document styling.", "invoice"); + invoice("invoice-classic-v2", "v2/ClassicInvoiceV2Example", "Classic Invoice", "The ClassicInvoice preset — letterhead header band, TOTAL DUE hero strip, BILL TO / FROM columns, and a dedicated Summary table after the line items.", "invoice"); // ===== Templates / Proposal ===== proposal("proposal-cinematic", "ProposalCinematicFileExample", "Cinematic Proposal", "Layered ModernProposal layout with cover panel, hero spread, and rich typography.", "proposal", "cinematic"); diff --git a/examples/src/main/java/com/demcha/examples/templates/invoice/v2/ClassicInvoiceV2Example.java b/examples/src/main/java/com/demcha/examples/templates/invoice/v2/ClassicInvoiceV2Example.java new file mode 100644 index 000000000..19b4e9a23 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/invoice/v2/ClassicInvoiceV2Example.java @@ -0,0 +1,61 @@ +package com.demcha.examples.templates.invoice; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.core.theme.BrandTheme; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; +import com.demcha.compose.document.templates.invoice.presets.ClassicInvoice; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the layered {@code invoice.v2} Classic Invoice preset against + * the shared {@code InvoiceDocumentSpec} sample data using the default + * {@code BrandTheme.invoiceModern()} theme. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/invoice/invoice-classic-v2.pdf}.

+ * + *

The preset pads the page flow itself, so the session margin stays at + * {@code ClassicInvoice.RECOMMENDED_MARGIN} (zero) and the page keeps its + * plain white background — the letterhead look of the preset.

+ */ +public final class ClassicInvoiceV2Example { + + private ClassicInvoiceV2Example() { + } + + /** + * @return absolute path of the rendered PDF + * @throws Exception if rendering fails + */ + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/invoice", "invoice-classic-v2.pdf"); + InvoiceDocumentSpec spec = ExampleDataFactory.sampleInvoice(); + DocumentTemplate template = + ClassicInvoice.create(BrandTheme.invoiceModern()); + + float m = (float) ClassicInvoice.RECOMMENDED_MARGIN; + try (DocumentSession document = GraphCompose.document(outputFile) + .pageSize(DocumentPageSize.A4) + .margin(m, m, m, m) + .create()) { + template.compose(document, spec); + document.buildPdf(); + } + return outputFile; + } + + /** + * @param args ignored + * @throws Exception if rendering fails + */ + public static void main(String[] args) throws Exception { + System.out.println("Generated: " + generate()); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoiceLayoutSnapshotTest.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoiceLayoutSnapshotTest.java new file mode 100644 index 000000000..140c2f61f --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoiceLayoutSnapshotTest.java @@ -0,0 +1,58 @@ +package com.demcha.compose.document.templates.invoice.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.TemplateTestSupport; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Exact layout snapshot gate for {@link ClassicInvoice} — freezes the + * resolved node geometry the pixel budget cannot see (a small column or + * spacing shift stays under the visual-diff budget but changes the + * snapshot), and the pagination contract of the overflow invoice: the + * forty-line-item fixture flows onto a second page with the repeated + * table header, and the summary + footer follow the table. + * + *

The multi-page contract is guarded here at snapshot level only — + * full-page pixel baselines drift across platforms far more than the + * geometry they would guard (see the budget note in + * {@code InvoiceV2VisualParityTest}), while the snapshot is exact on + * every platform.

+ * + *

Refresh with {@code -Dgraphcompose.updateSnapshots=true} after a + * deliberate layout change, and commit the JSON with the change.

+ */ +class ClassicInvoiceLayoutSnapshotTest { + + private static DocumentSession open() { + float m = (float) ClassicInvoice.RECOMMENDED_MARGIN; + return GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(m, m, m, m) + .create(); + } + + @Test + void canonicalInvoiceMatchesLayoutSnapshot() throws Exception { + try (DocumentSession session = open()) { + ClassicInvoice.create().compose(session, InvoicePresetFixtures.canonicalInvoice()); + assertThat(session.layoutSnapshot().totalPages()).isEqualTo(1); + TemplateTestSupport.assertCanonicalSnapshot( + session, "classic_invoice_layout", "invoice"); + } + } + + @Test + void overflowInvoicePaginatesOntoSecondPage() throws Exception { + try (DocumentSession session = open()) { + ClassicInvoice.create().compose(session, InvoicePresetFixtures.stressInvoice()); + assertThat(session.layoutSnapshot().totalPages()).isEqualTo(2); + TemplateTestSupport.assertCanonicalSnapshot( + session, "classic_invoice_stress_layout", "invoice"); + } + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoiceSmokeTest.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoiceSmokeTest.java new file mode 100644 index 000000000..2b1776029 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoiceSmokeTest.java @@ -0,0 +1,83 @@ +package com.demcha.compose.document.templates.invoice.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.core.theme.BrandTheme; +import com.demcha.compose.document.templates.data.invoice.InvoiceData; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Smoke test for the layered invoice pipeline through + * {@link ClassicInvoice} — proves the preset renders an + * {@link InvoiceDocumentSpec} end-to-end on a {@link BrandTheme}, via + * both factory variants, with any theme, and on an empty invoice. + */ +class ClassicInvoiceSmokeTest { + + /** An invoice with no line items, summaries, notes, footer, or status — the empty paths. */ + private static InvoiceDocumentSpec minimalSpec() { + return InvoiceDocumentSpec.from(InvoiceData.builder() + .invoiceNumber("GC-2026-002") + .fromParty(from -> from.name("GraphCompose Studio")) + .billToParty(to -> to.name("Northwind Systems")) + .build()); + } + + private static void render(DocumentTemplate template, + InvoiceDocumentSpec spec) throws Exception { + float m = (float) ClassicInvoice.RECOMMENDED_MARGIN; + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(m, m, m, m) + .create()) { + template.compose(session, spec); + assertThat(session.roots()).isNotEmpty(); + // Drive layout + render, not just composition — the zero-row + // summary table of an empty invoice only exists at layout time. + assertThat(session.toPdfBytes()).isNotEmpty(); + } + } + + @Test + void exposesStableIdentity() { + DocumentTemplate template = ClassicInvoice.create(); + assertThat(template.id()).isEqualTo(ClassicInvoice.ID); + assertThat(template.displayName()).isEqualTo(ClassicInvoice.DISPLAY_NAME); + } + + @Test + void defaultFactoryRendersWithInvoiceTheme() throws Exception { + // create() wires BrandTheme.invoiceModern() — the variant the example uses. + render(ClassicInvoice.create(), InvoicePresetFixtures.canonicalInvoice()); + } + + @Test + void rendersWithExplicitTheme() throws Exception { + render(ClassicInvoice.create(BrandTheme.invoiceModern()), + InvoicePresetFixtures.canonicalInvoice()); + } + + @Test + void readsAnyTheme() throws Exception { + // Renders under a non-invoice theme without crashing: the header, + // hero, labels, and footer follow the theme; the line-item body + // cells inherit the DSL default (as in ModernInvoice). + render(ClassicInvoice.create(BrandTheme.boxedClassic()), + InvoicePresetFixtures.canonicalInvoice()); + } + + @Test + void rendersEmptyInvoice() throws Exception { + // Exercises the empty-collection paths through full layout and + // render: the skipped Summary section (the engine rejects a + // zero-row table), the empty note / payment lists under their + // always-rendered headings, the skipped footer-note paragraph, + // and the em-dash status fallback. + render(ClassicInvoice.create(), minimalSpec()); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoicePresetFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoicePresetFixtures.java new file mode 100644 index 000000000..e5b401de9 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoicePresetFixtures.java @@ -0,0 +1,112 @@ +package com.demcha.compose.document.templates.invoice.presets; + +import com.demcha.compose.document.templates.data.invoice.InvoiceData; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; + +/** + * Shared fixture data for the invoice preset gates — the SAME specs feed + * the pixel parity test and the layout snapshot test, so a geometry shift + * the pixel budget absorbs still trips the exact snapshot, and vice versa. + */ +final class InvoicePresetFixtures { + + private InvoicePresetFixtures() { + } + + /** + * Canonical sample invoice — exercises the hero, both parties, a + * multi-row line-items table, subtotal / tax / total summary, and the + * notes / payment-terms footer. Kept in qa so the gates depend only + * on main + main-test code. + */ + static InvoiceDocumentSpec canonicalInvoice() { + return InvoiceDocumentSpec.from(InvoiceData.builder() + .title("Invoice") + .invoiceNumber("GC-2026-041") + .issueDate("02 Apr 2026") + .dueDate("16 Apr 2026") + .status("Pending") + .fromParty(from -> from + .name("GraphCompose Studio") + .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") + .email("billing@graphcompose.dev") + .phone("+44 20 5555 1000") + .taxId("GB-99887766")) + .billToParty(to -> to + .name("Northwind Systems") + .addressLines("Attn: Finance Team", "410 Market Avenue", "Manchester, UK") + .email("ap@northwind.example") + .phone("+44 161 555 2200") + .taxId("NW-2026-01")) + .lineItem("Discovery workshop", "Stakeholder interviews", + "1", "GBP 1,450", "GBP 1,450") + .lineItem("Template architecture", "Reusable document flows", + "2", "GBP 980", "GBP 1,960") + .lineItem("Render QA", "Cross-platform pixel diffing", + "3", "GBP 320", "GBP 960") + .lineItem("Developer enablement", "Authoring docs + examples", + "1", "GBP 780", "GBP 780") + .summaryRow("Subtotal", "GBP 5,150") + .summaryRow("VAT (20%)", "GBP 1,030") + .totalRow("Total", "GBP 6,180") + .note("Please include the invoice number on your remittance advice.") + .note("All work was delivered as agreed during the April implementation window.") + .paymentTerm("Payment due within 14 calendar days.") + .paymentTerm("Bank transfer preferred; contact billing@graphcompose.dev for remittance details.") + .paymentTerm("Late payments may delay additional template customization work.") + .footerNote("Thank you for choosing GraphCompose for production document rendering.") + .build()); + } + + /** + * Overflow invoice — forty line items so the table paginates naturally + * and the repeated header + cross-page flow become part of the frozen + * contract. No manual page breaks. + */ + static InvoiceDocumentSpec stressInvoice() { + String[] services = { + "Discovery workshop", "Design system audit", "Template architecture", + "Layout engine tuning", "Render QA pass", "Accessibility review", + "Font pipeline setup", "Chart integration", "Data mapping", + "Pagination hardening", "Visual regression wiring", "Docs authoring", + "Stakeholder demo", "Performance profiling", "Release engineering", + "Support retainer"}; + InvoiceData.Builder builder = InvoiceData.builder() + .title("Invoice") + .invoiceNumber("GC-2026-042") + .issueDate("04 May 2026") + .dueDate("18 May 2026") + .status("Pending") + .fromParty(from -> from + .name("GraphCompose Studio") + .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") + .email("billing@graphcompose.dev") + .phone("+44 20 5555 1000")) + .billToParty(to -> to + .name("Northwind Systems") + .addressLines("Attn: Finance Team", "410 Market Avenue", "Manchester, UK") + .email("ap@northwind.example") + .phone("+44 161 555 2200")); + for (int i = 0; i < 40; i++) { + String service = services[i % services.length]; + int quantity = 1 + i % 4; + int unit = 180 + (i * 37) % 900; + builder.lineItem( + service + " — sprint " + (1 + i / services.length), + "Delivered under the framework agreement", + String.valueOf(quantity), + "GBP " + unit, + "GBP " + (quantity * unit)); + } + return InvoiceDocumentSpec.from(builder + .summaryRow("Subtotal", "GBP 44,890") + .summaryRow("VAT (20%)", "GBP 8,978") + .totalRow("Total", "GBP 53,868") + .note("Sprints 1-3 were delivered under the framework agreement.") + .note("Hardware and third-party licences are billed separately.") + .paymentTerm("Payment due within 14 calendar days.") + .paymentTerm("Bank transfer preferred; contact billing@graphcompose.dev for remittance details.") + .footerNote("Thank you for choosing GraphCompose for production document rendering.") + .build()); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceV2VisualParityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceV2VisualParityTest.java index 05c05ba7b..b04bd4c37 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceV2VisualParityTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceV2VisualParityTest.java @@ -4,7 +4,6 @@ import com.demcha.compose.document.api.DocumentPageSize; import com.demcha.compose.document.api.DocumentSession; import com.demcha.compose.document.templates.api.DocumentTemplate; -import com.demcha.compose.document.templates.data.invoice.InvoiceData; import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; import com.demcha.compose.testing.visual.PdfVisualRegression; import org.junit.jupiter.params.ParameterizedTest; @@ -69,51 +68,17 @@ private static Stream presets() { return Stream.of( Arguments.of("modern_invoice", ModernInvoice.RECOMMENDED_MARGIN, - (Supplier>) ModernInvoice::create)); + (Supplier>) ModernInvoice::create), + Arguments.of("classic_invoice", + ClassicInvoice.RECOMMENDED_MARGIN, + (Supplier>) ClassicInvoice::create)); } /** - * Canonical sample invoice — exercises the hero, both parties, a - * multi-row line-items table, subtotal / tax / total summary, and the - * notes / payment-terms footer. Kept inline so the test depends only - * on main + main-test code. + * Canonical sample invoice — shared with the layout snapshot gate via + * {@link InvoicePresetFixtures}, so both gates freeze the same render. */ private static InvoiceDocumentSpec canonicalInvoice() { - return InvoiceDocumentSpec.from(InvoiceData.builder() - .title("Invoice") - .invoiceNumber("GC-2026-041") - .issueDate("02 Apr 2026") - .dueDate("16 Apr 2026") - .status("Pending") - .fromParty(from -> from - .name("GraphCompose Studio") - .addressLines("18 Layout Street", "London, UK", "EC1A 4GC") - .email("billing@graphcompose.dev") - .phone("+44 20 5555 1000") - .taxId("GB-99887766")) - .billToParty(to -> to - .name("Northwind Systems") - .addressLines("Attn: Finance Team", "410 Market Avenue", "Manchester, UK") - .email("ap@northwind.example") - .phone("+44 161 555 2200") - .taxId("NW-2026-01")) - .lineItem("Discovery workshop", "Stakeholder interviews", - "1", "GBP 1,450", "GBP 1,450") - .lineItem("Template architecture", "Reusable document flows", - "2", "GBP 980", "GBP 1,960") - .lineItem("Render QA", "Cross-platform pixel diffing", - "3", "GBP 320", "GBP 960") - .lineItem("Developer enablement", "Authoring docs + examples", - "1", "GBP 780", "GBP 780") - .summaryRow("Subtotal", "GBP 5,150") - .summaryRow("VAT (20%)", "GBP 1,030") - .totalRow("Total", "GBP 6,180") - .note("Please include the invoice number on your remittance advice.") - .note("All work was delivered as agreed during the April implementation window.") - .paymentTerm("Payment due within 14 calendar days.") - .paymentTerm("Bank transfer preferred; contact billing@graphcompose.dev for remittance details.") - .paymentTerm("Late payments may delay additional template customization work.") - .footerNote("Thank you for choosing GraphCompose for production document rendering.") - .build()); + return InvoicePresetFixtures.canonicalInvoice(); } } diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/classic_invoice_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/classic_invoice_layout.json new file mode 100644 index 000000000..2bfc96b9b --- /dev/null +++ b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/classic_invoice_layout.json @@ -0,0 +1,1007 @@ +{ + "formatVersion" : "2.0", + "canvas" : { + "pageWidth" : 595.276, + "pageHeight" : 841.89, + "innerWidth" : 595.276, + "innerHeight" : 841.89, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, + "totalPages" : 1, + "nodes" : [ { + "path" : "Invoice[0]", + "entityName" : "Invoice", + "entityKind" : "ContainerNode", + "parentPath" : null, + "childIndex" : 0, + "depth" : 1, + "layer" : 1, + "computedX" : 0.0, + "computedY" : 191.765, + "placementX" : 0.0, + "placementY" : 191.765, + "placementWidth" : 595.276, + "placementHeight" : 650.125, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 595.276, + "contentHeight" : 650.125, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 24.0, + "right" : 24.0, + "bottom" : 24.0, + "left" : 24.0 + } + }, { + "path" : "Invoice[0]/Header[0]", + "entityName" : "Header", + "entityKind" : "RowNode", + "parentPath" : "Invoice[0]", + "childIndex" : 0, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 769.49, + "placementX" : 24.0, + "placementY" : 769.49, + "placementWidth" : 547.276, + "placementHeight" : 48.4, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 547.276, + "contentHeight" : 48.4, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderLeft[0]", + "entityName" : "HeaderLeft", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Header[0]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 790.915, + "placementX" : 24.0, + "placementY" : 790.915, + "placementWidth" : 185.13, + "placementHeight" : 26.975, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 185.13, + "contentHeight" : 26.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderLeft[0]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderLeft[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 802.165, + "placementX" : 24.0, + "placementY" : 802.165, + "placementWidth" : 185.13, + "placementHeight" : 15.725, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 185.13, + "contentHeight" : 15.725, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderLeft[0]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderLeft[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 790.915, + "placementX" : 24.0, + "placementY" : 790.915, + "placementWidth" : 73.38, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 73.38, + "contentHeight" : 9.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]", + "entityName" : "HeaderRight", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Header[0]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 306.638, + "computedY" : 769.49, + "placementX" : 306.638, + "placementY" : 769.49, + "placementWidth" : 115.136, + "placementHeight" : 48.4, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.136, + "contentHeight" : 48.4, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderRight[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 791.99, + "placementX" : 306.638, + "placementY" : 791.99, + "placementWidth" : 115.136, + "placementHeight" : 25.9, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.136, + "contentHeight" : 25.9, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderRight[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 780.74, + "placementX" : 306.638, + "placementY" : 780.74, + "placementWidth" : 60.58, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 60.58, + "contentHeight" : 9.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderRight[1]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 769.49, + "placementX" : 306.638, + "placementY" : 769.49, + "placementWidth" : 86.72, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 86.72, + "contentHeight" : 9.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Hero[1]", + "entityName" : "Hero", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]", + "childIndex" : 1, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 696.365, + "placementX" : 24.0, + "placementY" : 696.365, + "placementWidth" : 538.468, + "placementHeight" : 57.125, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 538.468, + "contentHeight" : 57.125, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 14.0, + "right" : 14.0, + "bottom" : 14.0, + "left" : 14.0 + } + }, { + "path" : "Invoice[0]/Hero[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Hero[1]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 38.0, + "computedY" : 729.315, + "placementX" : 38.0, + "placementY" : 729.315, + "placementWidth" : 62.942, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 62.942, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Hero[1]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Hero[1]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 38.0, + "computedY" : 710.365, + "placementX" : 38.0, + "placementY" : 710.365, + "placementWidth" : 510.468, + "placementHeight" : 12.95, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 510.468, + "contentHeight" : 12.95, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]", + "entityName" : "Parties", + "entityKind" : "RowNode", + "parentPath" : "Invoice[0]", + "childIndex" : 2, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 599.94, + "placementX" : 24.0, + "placementY" : 599.94, + "placementWidth" : 547.276, + "placementHeight" : 80.425, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 547.276, + "contentHeight" : 80.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]", + "entityName" : "BillTo", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Parties[2]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 599.94, + "placementX" : 24.0, + "placementY" : 599.94, + "placementWidth" : 115.709, + "placementHeight" : 80.425, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.709, + "contentHeight" : 80.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/BillTo[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 670.19, + "placementX" : 24.0, + "placementY" : 670.19, + "placementWidth" : 42.779, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 42.779, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/BillTo[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 658.015, + "placementX" : 24.0, + "placementY" : 658.015, + "placementWidth" : 102.685, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 102.685, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/BillTo[0]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 599.94, + "placementX" : 24.0, + "placementY" : 599.94, + "placementWidth" : 115.709, + "placementHeight" : 56.075, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.709, + "contentHeight" : 56.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]", + "entityName" : "From", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Parties[2]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 306.638, + "computedY" : 599.94, + "placementX" : 306.638, + "placementY" : 599.94, + "placementWidth" : 132.825, + "placementHeight" : 80.425, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 132.825, + "contentHeight" : 80.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/From[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 670.19, + "placementX" : 306.638, + "placementY" : 670.19, + "placementWidth" : 32.384, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 32.384, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/From[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 658.015, + "placementX" : 306.638, + "placementY" : 658.015, + "placementWidth" : 119.79, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 119.79, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/From[1]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 599.94, + "placementX" : 306.638, + "placementY" : 599.94, + "placementWidth" : 132.825, + "placementHeight" : 56.075, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 132.825, + "contentHeight" : 56.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/LineItems[3]", + "entityName" : "LineItems", + "entityKind" : "TableNode", + "parentPath" : "Invoice[0]", + "childIndex" : 3, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 449.965, + "placementX" : 24.0, + "placementY" : 449.965, + "placementWidth" : 400.84, + "placementHeight" : 133.975, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 400.84, + "contentHeight" : 133.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Summary[4]", + "entityName" : "Summary", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]", + "childIndex" : 4, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 353.89, + "placementX" : 24.0, + "placementY" : 353.89, + "placementWidth" : 262.0, + "placementHeight" : 80.075, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 262.0, + "contentHeight" : 80.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Summary[4]/SummaryTable[0]", + "entityName" : "SummaryTable", + "entityKind" : "TableNode", + "parentPath" : "Invoice[0]/Summary[4]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 353.89, + "placementX" : 24.0, + "placementY" : 353.89, + "placementWidth" : 262.0, + "placementHeight" : 80.075, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 262.0, + "contentHeight" : 80.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]", + "entityName" : "Footer", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]", + "childIndex" : 5, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 215.765, + "placementX" : 24.0, + "placementY" : 215.765, + "placementWidth" : 547.276, + "placementHeight" : 122.125, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 547.276, + "contentHeight" : 122.125, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]", + "entityName" : "FooterRow", + "entityKind" : "RowNode", + "parentPath" : "Invoice[0]/Footer[5]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 247.015, + "placementX" : 24.0, + "placementY" : 247.015, + "placementWidth" : 547.276, + "placementHeight" : 90.875, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 547.276, + "contentHeight" : 90.875, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]", + "entityName" : "InvoiceNotes", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 272.915, + "placementX" : 24.0, + "placementY" : 272.915, + "placementWidth" : 264.214, + "placementHeight" : 64.975, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 264.214, + "contentHeight" : 64.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 8.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 32.0, + "computedY" : 327.715, + "placementX" : 32.0, + "placementY" : 327.715, + "placementWidth" : 30.558, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 30.558, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]/ListNode[1]", + "entityName" : null, + "entityKind" : "ListNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 32.0, + "computedY" : 272.915, + "placementX" : 32.0, + "placementY" : 272.915, + "placementWidth" : 256.214, + "placementHeight" : 51.8, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 256.214, + "contentHeight" : 51.8, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]", + "entityName" : "InvoicePaymentTerms", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 247.015, + "placementX" : 306.638, + "placementY" : 247.015, + "placementWidth" : 253.35, + "placementHeight" : 90.875, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 253.35, + "contentHeight" : 90.875, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 8.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 314.638, + "computedY" : 327.715, + "placementX" : 314.638, + "placementY" : 327.715, + "placementWidth" : 78.859, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 78.859, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]/ListNode[1]", + "entityName" : null, + "entityKind" : "ListNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 314.638, + "computedY" : 247.015, + "placementX" : 314.638, + "placementY" : 247.015, + "placementWidth" : 245.35, + "placementHeight" : 77.7, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 245.35, + "contentHeight" : 77.7, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Footer[5]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 215.765, + "placementX" : 24.0, + "placementY" : 215.765, + "placementWidth" : 335.71, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 335.71, + "contentHeight" : 9.25, + "margin" : { + "top" : 14.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + } ] +} diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/classic_invoice_stress_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/classic_invoice_stress_layout.json new file mode 100644 index 000000000..e76739bd6 --- /dev/null +++ b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/classic_invoice_stress_layout.json @@ -0,0 +1,1007 @@ +{ + "formatVersion" : "2.0", + "canvas" : { + "pageWidth" : 595.276, + "pageHeight" : 841.89, + "innerWidth" : 595.276, + "innerHeight" : 841.89, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, + "totalPages" : 2, + "nodes" : [ { + "path" : "Invoice[0]", + "entityName" : "Invoice", + "entityKind" : "ContainerNode", + "parentPath" : null, + "childIndex" : 0, + "depth" : 1, + "layer" : 1, + "computedX" : 0.0, + "computedY" : -752.535, + "placementX" : 0.0, + "placementY" : -752.535, + "placementWidth" : 595.276, + "placementHeight" : 1594.425, + "startPage" : 0, + "endPage" : 1, + "contentWidth" : 595.276, + "contentHeight" : 1594.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 24.0, + "right" : 24.0, + "bottom" : 24.0, + "left" : 24.0 + } + }, { + "path" : "Invoice[0]/Header[0]", + "entityName" : "Header", + "entityKind" : "RowNode", + "parentPath" : "Invoice[0]", + "childIndex" : 0, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 769.49, + "placementX" : 24.0, + "placementY" : 769.49, + "placementWidth" : 547.276, + "placementHeight" : 48.4, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 547.276, + "contentHeight" : 48.4, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderLeft[0]", + "entityName" : "HeaderLeft", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Header[0]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 790.915, + "placementX" : 24.0, + "placementY" : 790.915, + "placementWidth" : 185.13, + "placementHeight" : 26.975, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 185.13, + "contentHeight" : 26.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderLeft[0]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderLeft[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 802.165, + "placementX" : 24.0, + "placementY" : 802.165, + "placementWidth" : 185.13, + "placementHeight" : 15.725, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 185.13, + "contentHeight" : 15.725, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderLeft[0]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderLeft[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 790.915, + "placementX" : 24.0, + "placementY" : 790.915, + "placementWidth" : 73.38, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 73.38, + "contentHeight" : 9.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]", + "entityName" : "HeaderRight", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Header[0]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 306.638, + "computedY" : 769.49, + "placementX" : 306.638, + "placementY" : 769.49, + "placementWidth" : 115.136, + "placementHeight" : 48.4, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.136, + "contentHeight" : 48.4, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderRight[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 791.99, + "placementX" : 306.638, + "placementY" : 791.99, + "placementWidth" : 115.136, + "placementHeight" : 25.9, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.136, + "contentHeight" : 25.9, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderRight[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 780.74, + "placementX" : 306.638, + "placementY" : 780.74, + "placementWidth" : 60.58, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 60.58, + "contentHeight" : 9.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Header[0]/HeaderRight[1]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Header[0]/HeaderRight[1]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 769.49, + "placementX" : 306.638, + "placementY" : 769.49, + "placementWidth" : 90.05, + "placementHeight" : 9.25, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 90.05, + "contentHeight" : 9.25, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Hero[1]", + "entityName" : "Hero", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]", + "childIndex" : 1, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 696.365, + "placementX" : 24.0, + "placementY" : 696.365, + "placementWidth" : 544.712, + "placementHeight" : 57.125, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 544.712, + "contentHeight" : 57.125, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 14.0, + "right" : 14.0, + "bottom" : 14.0, + "left" : 14.0 + } + }, { + "path" : "Invoice[0]/Hero[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Hero[1]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 38.0, + "computedY" : 729.315, + "placementX" : 38.0, + "placementY" : 729.315, + "placementWidth" : 62.942, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 62.942, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Hero[1]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Hero[1]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 38.0, + "computedY" : 710.365, + "placementX" : 38.0, + "placementY" : 710.365, + "placementWidth" : 516.712, + "placementHeight" : 12.95, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 516.712, + "contentHeight" : 12.95, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]", + "entityName" : "Parties", + "entityKind" : "RowNode", + "parentPath" : "Invoice[0]", + "childIndex" : 2, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 599.94, + "placementX" : 24.0, + "placementY" : 599.94, + "placementWidth" : 547.276, + "placementHeight" : 80.425, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 547.276, + "contentHeight" : 80.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]", + "entityName" : "BillTo", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Parties[2]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 599.94, + "placementX" : 24.0, + "placementY" : 599.94, + "placementWidth" : 115.709, + "placementHeight" : 80.425, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.709, + "contentHeight" : 80.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/BillTo[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 670.19, + "placementX" : 24.0, + "placementY" : 670.19, + "placementWidth" : 42.779, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 42.779, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/BillTo[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 658.015, + "placementX" : 24.0, + "placementY" : 658.015, + "placementWidth" : 102.685, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 102.685, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/BillTo[0]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/BillTo[0]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 599.94, + "placementX" : 24.0, + "placementY" : 599.94, + "placementWidth" : 115.709, + "placementHeight" : 56.075, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 115.709, + "contentHeight" : 56.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]", + "entityName" : "From", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Parties[2]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 306.638, + "computedY" : 599.94, + "placementX" : 306.638, + "placementY" : 599.94, + "placementWidth" : 132.825, + "placementHeight" : 80.425, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 132.825, + "contentHeight" : 80.425, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/From[1]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 670.19, + "placementX" : 306.638, + "placementY" : 670.19, + "placementWidth" : 32.384, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 32.384, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/From[1]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 658.015, + "placementX" : 306.638, + "placementY" : 658.015, + "placementWidth" : 119.79, + "placementHeight" : 10.175, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 119.79, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Parties[2]/From[1]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Parties[2]/From[1]", + "childIndex" : 2, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 599.94, + "placementX" : 306.638, + "placementY" : 599.94, + "placementWidth" : 132.825, + "placementHeight" : 56.075, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 132.825, + "contentHeight" : 56.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/LineItems[3]", + "entityName" : "LineItems", + "entityKind" : "TableNode", + "parentPath" : "Invoice[0]", + "childIndex" : 3, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 18.765, + "placementX" : 24.0, + "placementY" : 18.765, + "placementWidth" : 475.502, + "placementHeight" : 1104.175, + "startPage" : 0, + "endPage" : 1, + "contentWidth" : 475.502, + "contentHeight" : 1104.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Summary[4]", + "entityName" : "Summary", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]", + "childIndex" : 4, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 180.64, + "placementX" : 24.0, + "placementY" : 180.64, + "placementWidth" : 262.0, + "placementHeight" : 80.075, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 262.0, + "contentHeight" : 80.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Summary[4]/SummaryTable[0]", + "entityName" : "SummaryTable", + "entityKind" : "TableNode", + "parentPath" : "Invoice[0]/Summary[4]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 180.64, + "placementX" : 24.0, + "placementY" : 180.64, + "placementWidth" : 262.0, + "placementHeight" : 80.075, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 262.0, + "contentHeight" : 80.075, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]", + "entityName" : "Footer", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]", + "childIndex" : 5, + "depth" : 2, + "layer" : 2, + "computedX" : 24.0, + "computedY" : 68.415, + "placementX" : 24.0, + "placementY" : 68.415, + "placementWidth" : 547.276, + "placementHeight" : 96.225, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 547.276, + "contentHeight" : 96.225, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]", + "entityName" : "FooterRow", + "entityKind" : "RowNode", + "parentPath" : "Invoice[0]/Footer[5]", + "childIndex" : 0, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 99.665, + "placementX" : 24.0, + "placementY" : 99.665, + "placementWidth" : 547.276, + "placementHeight" : 64.975, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 547.276, + "contentHeight" : 64.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]", + "entityName" : "InvoiceNotes", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]", + "childIndex" : 0, + "depth" : 4, + "layer" : 4, + "computedX" : 24.0, + "computedY" : 99.665, + "placementX" : 24.0, + "placementY" : 99.665, + "placementWidth" : 250.2, + "placementHeight" : 64.975, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 250.2, + "contentHeight" : 64.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 8.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 32.0, + "computedY" : 154.465, + "placementX" : 32.0, + "placementY" : 154.465, + "placementWidth" : 30.558, + "placementHeight" : 10.175, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 30.558, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]/ListNode[1]", + "entityName" : null, + "entityKind" : "ListNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoiceNotes[0]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 32.0, + "computedY" : 99.665, + "placementX" : 32.0, + "placementY" : 99.665, + "placementWidth" : 242.2, + "placementHeight" : 51.8, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 242.2, + "contentHeight" : 51.8, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]", + "entityName" : "InvoicePaymentTerms", + "entityKind" : "SectionNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]", + "childIndex" : 1, + "depth" : 4, + "layer" : 4, + "computedX" : 306.638, + "computedY" : 99.665, + "placementX" : 306.638, + "placementY" : 99.665, + "placementWidth" : 253.35, + "placementHeight" : 64.975, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 253.35, + "contentHeight" : 64.975, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 8.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]/ParagraphNode[0]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]", + "childIndex" : 0, + "depth" : 5, + "layer" : 5, + "computedX" : 314.638, + "computedY" : 154.465, + "placementX" : 314.638, + "placementY" : 154.465, + "placementWidth" : 78.859, + "placementHeight" : 10.175, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 78.859, + "contentHeight" : 10.175, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]/ListNode[1]", + "entityName" : null, + "entityKind" : "ListNode", + "parentPath" : "Invoice[0]/Footer[5]/FooterRow[0]/InvoicePaymentTerms[1]", + "childIndex" : 1, + "depth" : 5, + "layer" : 5, + "computedX" : 314.638, + "computedY" : 99.665, + "placementX" : 314.638, + "placementY" : 99.665, + "placementWidth" : 245.35, + "placementHeight" : 51.8, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 245.35, + "contentHeight" : 51.8, + "margin" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + }, { + "path" : "Invoice[0]/Footer[5]/ParagraphNode[1]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "Invoice[0]/Footer[5]", + "childIndex" : 1, + "depth" : 3, + "layer" : 3, + "computedX" : 24.0, + "computedY" : 68.415, + "placementX" : 24.0, + "placementY" : 68.415, + "placementWidth" : 335.71, + "placementHeight" : 9.25, + "startPage" : 1, + "endPage" : 1, + "contentWidth" : 335.71, + "contentHeight" : 9.25, + "margin" : { + "top" : 14.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + }, + "padding" : { + "top" : 0.0, + "right" : 0.0, + "bottom" : 0.0, + "left" : 0.0 + } + } ] +} diff --git a/qa/src/test/resources/visual-baselines/invoice-v2-layered/classic_invoice-page-0.png b/qa/src/test/resources/visual-baselines/invoice-v2-layered/classic_invoice-page-0.png new file mode 100644 index 000000000..a9681f812 Binary files /dev/null and b/qa/src/test/resources/visual-baselines/invoice-v2-layered/classic_invoice-page-0.png differ diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoice.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoice.java new file mode 100644 index 000000000..dd7090afe --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ClassicInvoice.java @@ -0,0 +1,461 @@ +package com.demcha.compose.document.templates.invoice.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.RowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.dsl.TableBuilder; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.document.table.DocumentTableStyle; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.core.theme.BrandTheme; +import com.demcha.compose.document.templates.data.invoice.InvoiceData; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; +import com.demcha.compose.document.templates.data.invoice.InvoiceLineItem; +import com.demcha.compose.document.templates.data.invoice.InvoiceParty; +import com.demcha.compose.document.templates.data.invoice.InvoiceSummaryRow; + +import java.util.List; +import java.util.Objects; + +/** + * Classic Invoice — the letterhead-style layered invoice preset. + * + *

An invoice with a document header band (company name + first + * address line on the left, a 28pt "INVOICE" title with number and + * issue date on the right), a "TOTAL DUE" hero strip, a + * BILL TO / FROM two-column party row, the line-items table, a + * dedicated Summary table composed after the line items + * (subtotal / tax / TOTAL, the last row emphasized), and a notes / + * payment-terms footer. Long invoices paginate naturally: the + * line-items table flows onto the next page with its header repeated, + * and the summary + footer follow the table. The preset carries its + * spacing on the page flow itself (16pt flow spacing, 24pt padding on + * every edge) and expects a zero session margin — see + * {@link #RECOMMENDED_MARGIN}.

+ * + *

Layout traits that distinguish it from {@link ModernInvoice} and are + * part of this preset's visual contract: the header band (Modern has + * none), the party order (BILL TO left, FROM right — Modern renders + * FROM first), the separate Summary table (Modern folds the summary rows + * into the line-items table; this one repeats the line-items column + * pattern and shrink-wraps to its own rows), the fully-rounded hero + * panel (Modern rounds the right edge only), and the dark table-header + * ink on the deep-teal fill (Modern uses the light surface colour). The + * party blocks join address lines, email, and phone only — no tax id + * line.

+ * + *

Ported-render fidelity: the preset reproduces the rendered output + * of the published standalone {@code invoice-classic} template. Three + * traits follow from that and are deliberate: the header always reads + * "INVOICE" — {@link InvoiceData#title()} is not rendered; the + * "TOTAL DUE" strip carries the invoice metadata (number, dates, + * status), not an amount; and the table-header ink stays dark on the + * teal fill. Changing any of them is a redesign of the ported look, + * not a port fix.

+ * + *

The preset reads every shared surface from the + * {@link BrandTheme#invoiceModern()} tokens (palette, Helvetica type + * scale, hero panel radius / padding / accent width). The two ink + * colours below and the geometry constants that have no theme slot + * stay preset-local.

+ */ +public final class ClassicInvoice { + + /** + * Stable template identifier. + */ + public static final String ID = "invoice-classic"; + + /** + * Human-readable display name. + */ + public static final String DISPLAY_NAME = "Classic Invoice"; + + /** + * Recommended session margin (in points). The preset pads the page + * flow itself (24pt on every edge), so the session margin stays zero; + * a non-zero margin would double the page frame. + */ + public static final double RECOMMENDED_MARGIN = 0.0; + + private static final double TABLE_PADDING = 7.0; + + /** + * Flow spacing between the header, hero, parties, table, summary, + * and footer blocks. Intentionally wider than the theme's + * {@code pageFlowSpacing} — the letterhead look breathes more. + */ + private static final double PAGE_SPACING = 16.0; + + /** + * Page-flow padding on every edge — the preset's page frame + * (replaces the session margin, see {@link #RECOMMENDED_MARGIN}). + */ + private static final double PAGE_PADDING = 24.0; + + /** + * Company-name size in the header band — the h2 step of the modern + * business type ladder (between the theme's 11pt banner and 28pt + * headline slots, which is why it has no {@code Typography} slot). + */ + private static final double COMPANY_NAME_SIZE = 17.0; + + /** + * Deep teal used for the INVOICE title, the company name, and the + * line-items table header fill (the modern business primary). + * Same value as {@link ModernInvoice}'s preset-local primary — kept + * preset-local because adding a {@code Palette} slot would change the + * record's public constructor; fold both into a shared slot when the + * palette can grow compatibly. + */ + private static final DocumentColor PRIMARY = DocumentColor.rgb(20, 60, 75); + + /** + * Gold accent for the hero strip, the status read-out, and the footer + * column rules (the modern business accent). Preset-local, same + * rationale as {@link #PRIMARY}. + */ + private static final DocumentColor ACCENT = DocumentColor.rgb(196, 153, 76); + + private ClassicInvoice() { + } + + /** + * Builds the preset with the modern business theme + * ({@link BrandTheme#invoiceModern()}). + * + * @return ready-to-use template + */ + public static DocumentTemplate create() { + return create(BrandTheme.invoiceModern()); + } + + /** + * Builds the preset with a caller-supplied theme, so callers can + * vary the invoice flavour (typography scale, palette) without + * forking this class. + * + * @param theme active theme + * @return ready-to-use template + */ + public static DocumentTemplate create(BrandTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private record Template(BrandTheme theme) implements DocumentTemplate { + + @Override + public String id() { + return ID; + } + + @Override + public String displayName() { + return DISPLAY_NAME; + } + + @Override + public void compose(DocumentSession document, InvoiceDocumentSpec spec) { + Objects.requireNonNull(document, "document"); + InvoiceData data = Objects.requireNonNull(spec, "spec").invoice(); + + DocumentColor panelFill = theme.palette().banner(); // soft tan + DocumentColor surface = theme.palette().mainFill(); // cream + DocumentColor rule = theme.palette().rule(); + + DocumentTextStyle titleStyle = DocumentTextStyle.builder() + .fontName(theme.typography().headlineFont()) + .size(theme.typography().sizeHeadline()) + .decoration(DocumentTextDecoration.BOLD) + .color(PRIMARY) + .build(); + DocumentTextStyle companyStyle = DocumentTextStyle.builder() + .fontName(theme.typography().headlineFont()) + .size(COMPANY_NAME_SIZE) + .decoration(DocumentTextDecoration.BOLD) + .color(PRIMARY) + .build(); + DocumentTextStyle labelStyle = theme.bodyBoldStyle(); + DocumentTextStyle bodyStyle = theme.bodyStyle(); + DocumentTextStyle captionStyle = DocumentTextStyle.builder() + .fontName(theme.typography().bodyFont()) + .size(theme.typography().sizeEntrySubtitle()) + .color(theme.palette().muted()) + .build(); + + // Line-item cells intentionally carry NO textStyle — they inherit + // the DSL default table-cell text (same guard as ModernInvoice). + DocumentTableStyle bordered = DocumentTableStyle.builder() + .stroke(DocumentStroke.of(rule, 0.6)) + .padding(DocumentInsets.of(TABLE_PADDING)) + .build(); + // The header keeps the dark body-bold ink on the deep-teal fill — + // the ported template's approved render (Modern uses the light + // surface ink instead). A higher-contrast header is a deliberate + // redesign of the ported look, not a port fix. + DocumentTableStyle headerStyle = DocumentTableStyle.builder() + .fillColor(PRIMARY) + .stroke(DocumentStroke.of(rule, 0.6)) + .padding(DocumentInsets.of(TABLE_PADDING + 1)) + .textStyle(labelStyle) + .build(); + DocumentTableStyle totalStyle = DocumentTableStyle.builder() + .fillColor(panelFill) + .stroke(DocumentStroke.of(rule, 0.6)) + .padding(DocumentInsets.of(TABLE_PADDING + 1)) + .textStyle(labelStyle) + .build(); + + PageFlowBuilder flow = document.dsl().pageFlow() + .name("Invoice") + .spacing(PAGE_SPACING) + .padding(DocumentInsets.of(PAGE_PADDING)) + .addRow("Header", row -> renderHeader(row, data, + companyStyle, titleStyle, captionStyle)) + .addSection("Hero", section -> renderHero(section, data, + panelFill, labelStyle)) + .addRow("Parties", row -> renderParties(row, data, + labelStyle, bodyStyle)) + .addTable(table -> renderLineItems(table, data, + bordered, headerStyle, panelFill, surface)); + // The engine rejects a zero-row table, so an invoice without + // summary rows renders without the Summary section. + if (!data.summaryRows().isEmpty()) { + flow.addSection("Summary", section -> renderSummary(section, data, + bordered, totalStyle)); + } + flow.addSection("Footer", section -> renderFooter(section, data, + labelStyle, captionStyle)) + .build(); + } + + private void renderHeader(RowBuilder row, + InvoiceData data, + DocumentTextStyle companyStyle, + DocumentTextStyle titleStyle, + DocumentTextStyle captionStyle) { + row.spacing(18); + row.weights(1, 1); + row.addSection("HeaderLeft", section -> section + .spacing(2) + .addParagraph(p -> p + .text(data.fromParty().name()) + .textStyle(companyStyle) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text(firstAddressLine(data.fromParty())) + .textStyle(captionStyle) + .margin(DocumentInsets.zero()))); + row.addSection("HeaderRight", section -> section + .spacing(2) + .addParagraph(p -> p + .text("INVOICE") + .textStyle(titleStyle) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text(safe(data.invoiceNumber())) + .textStyle(captionStyle) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text("Issued " + safe(data.issueDate())) + .textStyle(captionStyle) + .margin(DocumentInsets.zero()))); + } + + private void renderHero(SectionBuilder section, + InvoiceData data, + DocumentColor panelFill, + DocumentTextStyle labelStyle) { + // The strip is a metadata read-out — the ported template renders + // no amount after the TOTAL DUE label. + section.softPanel(panelFill, + theme.spacing().bannerCornerRadius(), + theme.spacing().bannerInnerPadding()) + .accentLeft(ACCENT, theme.spacing().accentRuleWidth()) + .spacing(6) + .addParagraph(p -> p + .text("TOTAL DUE") + .textStyle(labelStyle) + .margin(DocumentInsets.zero())) + .addRich(rich -> rich + .plain("Invoice ").bold(safe(data.invoiceNumber())) + .plain(" Issued ").bold(safe(data.issueDate())) + .plain(" Due ").bold(safe(data.dueDate())) + .plain(" Status ").accent(safeStatus(data.status()), ACCENT)); + } + + private void renderParties(RowBuilder row, + InvoiceData data, + DocumentTextStyle labelStyle, + DocumentTextStyle bodyStyle) { + row.spacing(18); + row.weights(1, 1); + row.addSection("BillTo", section -> renderContactBlock( + section, data.billToParty(), "BILL TO", labelStyle, bodyStyle)); + row.addSection("From", section -> renderContactBlock( + section, data.fromParty(), "FROM", labelStyle, bodyStyle)); + } + + private static void renderLineItems(TableBuilder table, + InvoiceData data, + DocumentTableStyle bordered, + DocumentTableStyle headerStyle, + DocumentColor zebraOdd, + DocumentColor zebraEven) { + table.name("LineItems") + .columns( + DocumentTableColumn.auto(), + DocumentTableColumn.fixed(54), + DocumentTableColumn.fixed(96), + DocumentTableColumn.fixed(96)) + .defaultCellStyle(bordered) + .headerRow("Description", "Qty", "Unit", "Amount") + .headerStyle(headerStyle) + .repeatHeader() + .zebra(zebraOdd, zebraEven); + // Only item.description() renders in the cell — the optional + // details string would drive the auto-sized description column + // past the inner page width (same guard as ModernInvoice). + for (InvoiceLineItem item : data.lineItems()) { + table.row(item.description(), item.quantity(), + item.unitPrice(), item.amount()); + } + } + + private static void renderSummary(SectionBuilder section, + InvoiceData data, + DocumentTableStyle bordered, + DocumentTableStyle totalStyle) { + // The summary lives in its own table that repeats the LineItems + // column pattern (auto / 54 / 96 / 96) — the ported template's + // shape. The table shrink-wraps to its own rows (the empty auto + // column collapses), so it renders narrower than the line items + // above. The LAST row is the grand total and renders via + // totalRow for the emphasized style. + section.addTable(summary -> { + summary.name("SummaryTable") + .columns( + DocumentTableColumn.auto(), + DocumentTableColumn.fixed(54), + DocumentTableColumn.fixed(96), + DocumentTableColumn.fixed(96)) + .defaultCellStyle(bordered); + List summaries = data.summaryRows(); + for (int i = 0; i < summaries.size(); i++) { + InvoiceSummaryRow row = summaries.get(i); + if (i == summaries.size() - 1) { + summary.totalRow(totalStyle, "", "", row.label(), row.value()); + } else { + summary.row("", "", row.label(), row.value()); + } + } + }); + } + + private void renderFooter(SectionBuilder section, + InvoiceData data, + DocumentTextStyle labelStyle, + DocumentTextStyle captionStyle) { + section.spacing(8) + .addRow("FooterRow", row -> row + .spacing(18) + .weights(1, 1) + .addSection("InvoiceNotes", col -> col + .accentLeft(ACCENT, 3) + .padding(0, 0, 0, 8) + .spacing(3) + .addParagraph(p -> p + .text("Notes") + .textStyle(labelStyle) + .margin(DocumentInsets.zero())) + .addList(list -> list.items(data.notes()))) + .addSection("InvoicePaymentTerms", col -> col + .accentLeft(ACCENT, 3) + .padding(0, 0, 0, 8) + .spacing(3) + .addParagraph(p -> p + .text("Payment terms") + .textStyle(labelStyle) + .margin(DocumentInsets.zero())) + .addList(list -> list.items(data.paymentTerms())))); + if (!data.footerNote().isBlank()) { + section.addParagraph(p -> p + .text(data.footerNote()) + .textStyle(captionStyle) + .margin(new DocumentInsets(14, 0, 0, 0))); + } + } + + private static void renderContactBlock(SectionBuilder section, + InvoiceParty party, + String label, + DocumentTextStyle labelStyle, + DocumentTextStyle bodyStyle) { + section.spacing(2) + .addParagraph(p -> p + .text(label) + .textStyle(labelStyle) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text(party.name()) + .textStyle(labelStyle) + .margin(DocumentInsets.zero())) + .addParagraph(p -> p + .text(joinAddress(party)) + .textStyle(bodyStyle) + .lineSpacing(1.3) + .margin(DocumentInsets.zero())); + } + } + + private static String firstAddressLine(InvoiceParty party) { + for (String line : party.addressLines()) { + if (line != null && !line.isBlank()) { + return line; + } + } + return ""; + } + + private static String joinAddress(InvoiceParty party) { + StringBuilder builder = new StringBuilder(); + for (String line : party.addressLines()) { + if (line == null || line.isBlank()) { + continue; + } + append(builder, line); + } + if (!party.email().isBlank()) { + append(builder, party.email()); + } + if (!party.phone().isBlank()) { + append(builder, party.phone()); + } + return builder.toString(); + } + + private static void append(StringBuilder builder, String line) { + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(line); + } + + private static String safeStatus(String status) { + if (status == null || status.isBlank()) { + return "—"; + } + return status; + } + + private static String safe(String value) { + return value == null ? "" : value; + } +}