Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,34 @@ follow semantic versioning; release dates are ISO 8601.

### Templates

- **A violet SaaS invoice preset: `WorkspaceInvoice`.** A brand masthead over a short
accent bar, a half-split issuer and metadata header, two addressed parties on filled
discs, a service-line table whose marks sit on coloured tiles, a settlement row
pairing the bank details against the totals, and a closing band carrying the wordmark.
Ships as `invoice.presets.WorkspaceInvoice` on the existing `StructuredInvoiceData`
model, porting the rendered layout of a published standalone template. Like
`PaymentsInvoice` it flows — the table's header repeats on every page it reaches, a
continuation page reserves a deeper bottom margin, and every page carries its number,
which is a departure from the one-page design and the only way a lost page is
detectable. The sheet's own closing band cannot be page chrome, because a header
footer zone takes only strings and the band holds a wordmark and a link, so it is body
content and the number is the only chrome. Two of the design's own type corrections
are carried as measured constants rather than smoothed away: a cap read off a
screenshot includes half a pixel of antialiasing on each edge, and the body face sets
uppercase about 6% wider than the design's at the same cap height — measured across
three all-caps runs while mixed-case runs at the same sizes matched within 2%. This
design states its currency once per money column and writes the figures under it bare,
carrying the code only on the total, which is the opposite of what `PaymentsInvoice`
does and is why the two presets format money differently. A quantity is written with
what it counts. Both parties print a registration under their address through the pair
`InvoiceRecipient` gained for it, and a party with no number prints no label. Guarded
by a smoke test (including the unknown-mark data error, a line with no mark, the
wordmark fallback, the currency named once and not on every figure, the quantity with
its unit, both registrations and the absent one, the closing address as an annotation,
a missing ship-to, a payment card with no note, an empty document, and a thirty-line
invoice that runs on, repeats its column names and numbers its pages), an exact layout
snapshot and a pixel-parity gate; the examples showcase gains `invoice-workspace-v2`.

- **A paginating invoice preset: `PaymentsInvoice`.** A lavender-and-navy sheet with a
diagonal band crossing the masthead, a half-split issuer and metadata header, two
addressed parties on discs, a marked service-line table, a settlement row pairing bank
Expand Down
Binary file added assets/readme/examples/invoice-workspace-v2.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
import com.demcha.examples.templates.invoice.ConsultingInvoiceV2Example;
import com.demcha.examples.templates.invoice.LumaStudioInvoiceV2Example;
import com.demcha.examples.templates.invoice.PaymentsInvoiceV2Example;
import com.demcha.examples.templates.invoice.WorkspaceInvoiceV2Example;
import com.demcha.examples.templates.invoice.InvoiceCinematicFileExample;
import com.demcha.examples.templates.invoice.ModernInvoiceV2Example;
import com.demcha.examples.templates.proposal.CinematicProposalFileExample;
Expand Down Expand Up @@ -192,6 +193,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + ConsultingInvoiceV2Example.generate());
System.out.println("Generated: " + LumaStudioInvoiceV2Example.generate());
System.out.println("Generated: " + PaymentsInvoiceV2Example.generate());
System.out.println("Generated: " + WorkspaceInvoiceV2Example.generate());

// Proposals
System.out.println("Generated: " + ProposalCinematicFileExample.generate());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
invoice("invoice-consulting-v2", "v2/ConsultingInvoiceV2Example", "Consulting Invoice", "The ConsultingInvoice preset on the structured invoice model — brand lockup with the caller's logo, labelled masthead metadata, priced service lines with service periods, a totals stack and bank payment fields.", "invoice");
invoice("invoice-luma-studio-v2", "v2/LumaStudioInvoiceV2Example", "Luma Studio Invoice", "The LumaStudioInvoice preset on the structured invoice model — a cream sidebar carrying the brand lockup and its ornament, a billed-to / shipped-to pair, priced service lines with a VAT column, and the notes and bank details above a sign-off band.", "invoice");
invoice("invoice-payments-v2", "v2/PaymentsInvoiceV2Example", "Payments Invoice", "The PaymentsInvoice preset on the structured invoice model — a diagonal band crossing the masthead, a half-split issuer and metadata header, two addressed parties, marked service lines that repeat their header across pages, and a settlement row pairing bank details against the totals.", "invoice");
invoice("invoice-workspace-v2", "v2/WorkspaceInvoiceV2Example", "Workspace Invoice", "The WorkspaceInvoice preset on the structured invoice model — a brand masthead over an accent bar, a half-split issuer and metadata header, two addressed parties on discs, service lines whose marks sit on coloured tiles, and a settlement row above a closing band.", "invoice");
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");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.demcha.examples.support;

import com.demcha.compose.document.templates.data.invoice.InvoiceBrand;
import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock;
import com.demcha.compose.document.templates.data.invoice.InvoiceMasthead;
import com.demcha.compose.document.templates.data.invoice.InvoiceNotesBlock;
import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock;
import com.demcha.compose.document.templates.data.invoice.InvoiceRecipient;
import com.demcha.compose.document.templates.data.invoice.InvoiceServiceLines;
import com.demcha.compose.document.templates.data.invoice.InvoiceSummaryBlock;
import com.demcha.compose.document.templates.data.invoice.InvoiceTotalsBlock;
import com.demcha.compose.document.templates.data.invoice.StructuredInvoiceData;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
* Shared sample data for the Workspace invoice example.
*
* <p>Kept in lockstep with the qa module's {@code WorkspaceInvoiceFixtures} —
* the two modules cannot share a source file, so a content change here belongs
* there too.</p>
*/
public final class WorkspaceInvoiceSampleData {

private WorkspaceInvoiceSampleData() {
}

/**
* The sample invoice — six metadata rows, two addressed parties, four
* marked service lines, eight payment fields, two summed rows and a closing
* line with an address inside it.
*
* @return the document the example renders
*/
public static StructuredInvoiceData sample() {
return new StructuredInvoiceData(
brand(), supplier(), masthead(), billTo(), shipTo(),
new InvoiceSummaryBlock("", "", ""),
serviceLines(), totals(), payment(), notes(), "USD");
}

/** No logo: the wordmark is what a document without one falls back to. */
private static InvoiceBrand brand() {
return new InvoiceBrand(null, "kestrel", "", "", "", "");
}

private static InvoiceContactBlock supplier() {
return new InvoiceContactBlock(
"Kestrel Collaboration, Inc.",
List.of("500 Howard Street, 6th Floor", "San Francisco, CA 94105",
"United States"),
"", "", "kestrel.example",
"", "",
"VAT ID:", "US 77-0560185");
}

private static InvoiceMasthead masthead() {
List<InvoiceMasthead.Entry> entries = new ArrayList<>();
entries.add(new InvoiceMasthead.Entry("Invoice Number:", "INV-2024-0003521", true));
entries.add(new InvoiceMasthead.Entry("Invoice Date:", "27 May 2024", false));
entries.add(new InvoiceMasthead.Entry("Billing Period:", "1 - 31 May 2024", false));
entries.add(new InvoiceMasthead.Entry("Workspace ID:", "T04FQ9L2M", false));
entries.add(new InvoiceMasthead.Entry("Payment Terms:", "Net 30", false));
entries.add(new InvoiceMasthead.Entry("Due Date:", "26 June 2024", false));
return new InvoiceMasthead("INVOICE", entries);
}

/** The registration is the labelled pair printed under the address. */
private static InvoiceRecipient billTo() {
return new InvoiceRecipient("BILL TO", "Bright Future Ltd.", "",
List.of("45 King Street", "Manchester M2 7AZ", "United Kingdom"),
"", "", "VAT ID:", "GB 987 6543 21");
}

private static InvoiceRecipient shipTo() {
return new InvoiceRecipient("SHIP TO", "Bright Future Ltd.", "",
List.of("Unit 4, Riverside Park", "Leeds LS1 4AP", "United Kingdom"),
"", "", "", "");
}

private static InvoiceServiceLines serviceLines() {
// The currency is not in the labels: the preset names it once per money
// column, so a document states the code and nothing else.
InvoiceServiceLines.Columns columns = new InvoiceServiceLines.Columns(
"", "DESCRIPTION", "PLAN / SERVICE", "QTY", "UNIT PRICE", "AMOUNT", "");
List<InvoiceServiceLines.Line> lines = new ArrayList<>();
lines.add(line(1, "Kestrel Business+", "Workspace subscription",
"Business+ Annual", "50", "Users", "8.75", "437.50", "grid"));
lines.add(line(2, "Enterprise Key Management", "Customer-managed encryption keys",
"Add-on", "1", "", "100.00", "100.00", "search"));
lines.add(line(3, "Priority Support", "Named engineer and response target",
"Standard", "1", "", "50.00", "50.00", "shield"));
lines.add(line(4, "Workflow Automation", "Templates and scheduled runs",
"Included", "1", "", "0.00", "0.00", "grid"));
return new InvoiceServiceLines(columns, lines);
}

private static InvoiceServiceLines.Line line(int number, String title, String description,
String plan, String quantity, String unit,
String unitPrice, String amount, String icon) {
return new InvoiceServiceLines.Line(number, title, description, plan,
new BigDecimal(quantity), unit, decimal(unitPrice), decimal(amount), "", icon);
}

private static InvoiceTotalsBlock totals() {
List<InvoiceTotalsBlock.Row> rows = new ArrayList<>();
rows.add(new InvoiceTotalsBlock.Row("Subtotal", decimal("587.50")));
rows.add(new InvoiceTotalsBlock.Row("Tax (0%)", decimal("0.00")));
return new InvoiceTotalsBlock(rows, "TOTAL DUE", decimal("587.50"));
}

private static InvoicePaymentBlock payment() {
List<InvoicePaymentBlock.Field> fields = new ArrayList<>();
fields.add(new InvoicePaymentBlock.Field("Bank Name:", "Bay Union Bank, N.A."));
fields.add(new InvoicePaymentBlock.Field("Account Name:", "Kestrel Collaboration, Inc."));
fields.add(new InvoicePaymentBlock.Field("Account Number:", "000000123456789"));
fields.add(new InvoicePaymentBlock.Field("Routing (ABA):", "021000021"));
fields.add(new InvoicePaymentBlock.Field("SWIFT / BIC:", "BAYUUS33"));
fields.add(new InvoicePaymentBlock.Field("Currency:", "USD"));
fields.add(new InvoicePaymentBlock.Field("Reference:", "INV-2024-0003521"));
fields.add(new InvoicePaymentBlock.Field("Bank Address:", "1 Market Plaza, San Francisco"));
return new InvoicePaymentBlock("PAYMENT DETAILS", fields,
"Please include the invoice number in your payment reference.",
"PAYMENT DUE BY",
"26 June 2024",
"", "");
}

private static InvoiceNotesBlock notes() {
return new InvoiceNotesBlock("Thank you for using Kestrel.",
List.of("Questions about this invoice? Reach us at billing@kestrel.example "
+ "and we will get back to you within one business day."),
"billing@kestrel.example", "");
}

private static BigDecimal decimal(String printed) {
return new BigDecimal(printed.replace(",", ""));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.demcha.examples.templates.invoice;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.data.invoice.StructuredInvoiceData;
import com.demcha.compose.document.templates.invoice.presets.WorkspaceInvoice;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.WorkspaceInvoiceSampleData;

import java.nio.file.Path;

/**
* Renders the layered {@code invoice.v2} Workspace preset against the design
* sample.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/invoice/invoice-workspace-v2.pdf}.</p>
*
* <p>The preset owns its page geometry and its pagination — the table's header
* repeats and a continuation page reserves a deeper bottom margin — so the
* session starts unconfigured. The sample brings no logo, so the lockup box
* beside the title is filled with the brand's name as a wordmark.</p>
*/
public final class WorkspaceInvoiceV2Example {

private WorkspaceInvoiceV2Example() {
}

/**
* @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-workspace-v2.pdf");
StructuredInvoiceData data = WorkspaceInvoiceSampleData.sample();
DocumentTemplate<StructuredInvoiceData> template = WorkspaceInvoice.create();

try (DocumentSession document = GraphCompose.document(outputFile).create()) {
template.compose(document, data);
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());
}
}
Loading
Loading