jkunz edaeadb798
Default (tags) / security (push) Failing after 4s
Default (tags) / test (push) Failing after 0s
Default (tags) / metadata (push) Skipped
v4.2.0
2026-09-27 15:59:39 +00:00
2026-09-27 15:59:39 +00:00
2026-09-27 15:59:39 +00:00
2026-09-27 15:59:39 +00:00
2026-09-27 15:59:39 +00:00
2025-12-11 09:26:49 +00:00

@design.estate/dees-document

A powerful TypeScript framework for dynamically generating professional business documents like invoices with web components and PDF export capabilities. 🧾

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Installation

pnpm install @design.estate/dees-document

Features

  • 📄 PDF Generation - Server-side PDF creation from structured data through smartpdf (system Chrome or Chromium)
  • 🎨 Web Components - Lit-based custom elements for document rendering
  • 🌍 Multi-language Support - Built-in translation system (EN, DE)
  • 💱 Currency-aware amounts - Prices are formatted in the document's currency with the locale of the document language
  • 📱 Automatic Pagination - Smart content overflow handling across pages
  • 💳 QR Payment Codes - EPC QR codes for SEPA payments
  • 🖨️ Print Mode - Optimized rendering for both screen and print
  • 📐 A4 Format - Precise DIN A4 document dimensions
  • ✨ Theming - Customizable colors, backgrounds, and branding

Usage

Server-Side PDF Generation

import { PdfService } from '@design.estate/dees-document';
import type { finance } from '@tsclass/tsclass';

// Initialize the PDF service
const pdfService = new PdfService({});
await pdfService.start();

// Create invoice data (tsclass 9 accounting document)
const invoice: finance.TInvoice = {
  type: 'accounting-doc',
  accountingDocType: 'invoice',
  accountingDocId: 'INV-2024-001',
  accountingDocStatus: 'issued',
  id: 'INV-2024-001',
  date: Date.now(),
  status: 'issued',
  language: 'EN',
  incidenceId: 'INV-2024-001',
  objectActions: [],
  currency: 'EUR',
  dueInDays: 30,
  reverseCharge: false,
  notes: [],
  from: {
    name: 'Your Company GmbH',
    type: 'company',
    description: '',
    status: 'active',
    foundedDate: { year: 2020, month: 1, day: 1 },
    address: {
      streetName: 'Business Street',
      houseNumber: '123',
      city: 'Berlin',
      country: 'Germany',
      postalCode: '10115',
    },
    sepaConnection: {
      bic: 'DEUTDEFF',
      iban: 'DE89370400440532013000',
    },
    registrationDetails: {
      vatId: 'DE123456789',
      registrationName: 'Amtsgericht Berlin',
      registrationId: 'HRB 12345',
    },
  },
  to: {
    name: 'Customer Inc.',
    type: 'company',
    description: '',
    status: 'active',
    foundedDate: { year: 2020, month: 1, day: 1 },
    address: {
      streetName: 'Client Avenue',
      houseNumber: '456',
      city: 'Munich',
      country: 'Germany',
      postalCode: '80331',
    },
    registrationDetails: {
      vatId: 'DE987654321',
      registrationName: 'Amtsgericht München',
      registrationId: 'HRB 54321',
    },
  },
  items: [
    {
      name: 'Web Development Services',
      unitQuantity: 40,
      unitNetPrice: 95,
      unitType: 'HUR', // UN/ECE Recommendation 20: hour
      vatPercentage: 19,
      position: 1,
    },
    {
      name: 'Hosting (Annual)',
      unitQuantity: 1,
      unitNetPrice: 299,
      unitType: 'ANN', // UN/ECE Recommendation 20: year
      vatPercentage: 19,
      position: 2,
    },
  ],
  subject: 'Invoice INV-2024-001',
  versionInfo: {
    type: 'final',
    version: '1.0.0',
  },
};

// Generate PDF
const pdfResult = await pdfService.createPdfFromLetterObject({
  letterData: invoice,
  documentSettings: {
    languageCode: 'EN',
    enableDefaultHeader: true,
    enableDefaultFooter: true,
    enableFoldMarks: true,
    dateStyle: 'long',
  },
});

// Save to file
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
const fs = new SmartFs(new SmartFsProviderNode());
await fs.file('./invoice.pdf').write(Buffer.from(pdfResult.buffer));

// Don't forget to stop the service when done
await pdfService.stop();

createPdfFromLetterObject() returns the IPdf of @push.rocks/smartpdf 6: the PDF in buffer, and in metadata.textExtraction its text as PDF.js reads it, a line break where a printed line ends and a blank line (PDF_TEXT_PAGE_SEPARATOR of smartpdf) between pages. PDF generation needs Node.js 22.13 or newer.

Document Settings

Customize document appearance with IDocumentSettings:

const documentSettings = {
  // Language for translations
  languageCode: 'DE', // 'EN' | 'DE' | 'ES'

  // Layout options
  enableDefaultHeader: true,
  enableDefaultFooter: true,
  enableFoldMarks: true,
  enableTopDraftText: true,
  enableInvoiceContractRefSection: true,

  // Display options
  vatGroupPositions: true,
  dateStyle: 'long', // 'short' | 'medium' | 'long' | 'full'

  // Theming
  theme: {
    colorPrimaryForeground: '#ffffff',
    colorPrimaryBackground: '#e4002b',
    colorAccentForeground: '#333333',
    colorAccentBackground: '#f5f5f5',
    pageBackground: 'url(your-watermark.png)',
    coverPageBackground: 'url(your-cover.png)',
  },
};

Web Component Usage

For browser-based document viewing:

import '@design.estate/dees-document/web';

// In your HTML/Lit template
html`
  <dedocument-viewer
    .letterData=${invoiceData}
    .documentSettings=${documentSettings}
  ></dedocument-viewer>
`;

// Or render the document directly
html`
  <dedocument-dedocument
    .letterData=${invoiceData}
    .documentSettings=${documentSettings}
  ></dedocument-dedocument>
`;

Translation System

The package includes a translation system for multi-language document generation:

import { translation } from '@design.estate/dees-document/shared';

// Translate document labels
translation.translate('DE', 'invoice@@totalGross'); // "Gesamtbetrag brutto"
translation.translate('EN', 'invoice@@totalGross'); // "Total gross"
translation.translate('FR', 'invoice@@totalGross'); // "Total gross" — unknown languages fall back to English, unknown keys return the key

Module Exports

The package provides multiple entry points:

// Node.js server-side (PDF generation)
import { PdfService } from '@design.estate/dees-document';
import { PdfService } from '@design.estate/dees-document/node';

// Browser web components
import '@design.estate/dees-document/web';

// Shared utilities and types
import { isAccountingDoc, isPaymentReminder, translation, unitName, interfaces } from '@design.estate/dees-document/shared';

// TypeScript interfaces
import type { IDocumentSettings } from '@design.estate/dees-document/interfaces';

Document Components

The framework includes these web components:

Component Description
<dedocument-dedocument> Main document container with pagination
<dedocument-viewer> Document viewer with controls
<dedocument-page> Single A4 page with scaling support
<dedocument-pageheader> Company logo and branding header
<dedocument-letterheader> Sender/recipient addresses
<dedocument-pagecontent> Main content area
<dedocument-pagefooter> Company info and page numbers
<dedocument-contentinvoice> Invoice-specific content layout
<dedocument-paymentcode> EPC QR code for SEPA payments

Accounting Document Types

accountingDocType decides what the letterhead and the content print. Every type prints its number under its own label and, when the letter carries one, the period of performance.

accountingDocType Intro sentence Payment terms and QR pay box relatedDocuments
invoice invoices the positions printed not printed
corrected-invoice invoices the positions, after the correction block printed not printed: the corrected invoice is named in the correction block
creditnote credits the positions omitted: the recipient owes nothing printed
debitnote charges the positions additionally printed printed
self-billed-invoice settles what the recipient supplied omitted: the issuer is the payer not printed

Handing an accounting document over

Every accounting document is a letter: from @tsclass/tsclass 9.6.0 on, business.TLetter is a simple letter or any finance.TAccountingDoc, and from 9.7.0 on a payment reminder as well (see Payment Reminders). A credit note therefore goes straight into PdfService.createPdfFromLetterObject(), into <dedocument-dedocument> and into <dedocument-viewer> — no cast on the way in. A credit note names the document it corrects, so fill relatedDocuments with the number and, when known, the issue date of that document:

import { PdfService } from '@design.estate/dees-document';
import type { business, finance } from '@tsclass/tsclass';

const creditNote: finance.TCreditNote = {
  ...invoice, // same envelope as the invoice above
  accountingDocType: 'creditnote',
  accountingDocId: 'CN-2024-014',
  id: 'CN-2024-014',
  subject: 'Cancellation of invoice INV-2024-001',
  relatedDocuments: [
    { relationType: 'corrects', documentId: 'INV-2024-001', issueDate: Date.now() },
  ],
  notes: ['Storno der Rechnung INV-2024-001 vom 15.01.2025.'],
};

// a credit note is a TLetter, so every entry point takes it as it is
const letter: business.TLetter = creditNote;

const pdfResult = await pdfService.createPdfFromLetterObject({
  letterData: creditNote,
  documentSettings: { languageCode: 'DE' },
});
html`
  <dedocument-viewer
    .letterData=${creditNote}
    .documentSettings=${documentSettings}
  ></dedocument-viewer>
`;

Holding a TLetter and needing the accounting document in it, use the exported type guard — it is the same predicate the letterhead and the content use to decide whether a document number, a period of performance and a payment request are printed:

import { isAccountingDoc } from '@design.estate/dees-document/shared';

if (isAccountingDoc(letter)) {
  letter.accountingDocType; // 'invoice' | 'creditnote' | 'debitnote' | 'self-billed-invoice'
}

A letter that is no accounting document prints neither a number, nor a period of performance, nor payment terms and the QR pay box. It is still laid out as an invoice: the invoice intro sentence and an empty positions table with zero sums are printed. Rendering simple-letter content is not supported by this version.

Corrected invoices

An invoice that lacked a mandatory detail or stated one incorrectly is corrected by a document that refers to it specifically and unambiguously (§ 31 Abs. 5 UStDV). finance.TCorrectedInvoice (accountingDocType: 'corrected-invoice', from @tsclass/tsclass 9.8.0 on; UNTDID 1001 code 384) is that document, and it prints as a complete invoice: every position, the totals, the payment terms and the QR pay box. The letterhead labels its number "Rechnungsberichtigung Nr." / "Corrected invoice number", and above the positions it prints:

  • the title with the reference: "Rechnungsberichtigung zur Rechnung Nr. RE-2025-0042 vom 10.01.25" / "Corrected invoice for invoice no. RE-2025-0042 of 1/10/25", from correctedInvoice.documentId and correctedInvoice.issueDate;
  • "Berichtigte Angaben:" / "Corrected details:" and one line per entry of corrections: the detail of § 14 Abs. 4 Satz 1 Nr. 1 to 9 UStG (or § 14a UStG) in words, and whether it was supplied ("ergänzt", defect: 'missing') or corrected ("berichtigt", defect: 'incorrect').

A corrected invoice you issue states both. A received one may state neither, or the number without the date; then only the title, or the title with the number, is printed, and no reference is made up.

const correctedInvoice: finance.TCorrectedInvoice = {
  ...invoice, // a complete invoice with its own number
  accountingDocType: 'corrected-invoice',
  accountingDocId: 'RE-2025-0043',
  id: 'RE-2025-0043',
  correctedInvoice: { documentId: 'RE-2025-0042', issueDate },
  corrections: [
    { detail: 'supplier-tax-number', defect: 'missing' },
    { detail: 'tax', defect: 'incorrect' },
  ],
};

Notes

Every accounting document type prints notes below the totals and above the payment terms: each non-empty note as its own paragraph, in array order. A note is plain text. Its line breaks are kept, markup is printed as typed, and empty or whitespace-only notes are skipped. A long note flows across pages like the positions do.

const invoice = {
  // ... other fields
  notes: [
    'Thank you for your order.',
    'Delivery terms: DAP Bremen.\nPlease quote the invoice number with your payment.',
  ],
};

Units

A position's unitType is the unit code of EN 16931 (BT-130): a code of UN/ECE Recommendation 20, or of Recommendation 21 with the prefix X (business rule BR-CL-23). An invoice states the quantity and the unit in words its reader understands (§ 14 Abs. 4 Satz 1 Nr. 5 UStG), so the positions print the unit's name in the document's language instead of the code, in the singular for a quantity of one and in the plural for any other quantity:

Code English German
C62 unit / units Einheit / Einheiten
H87, XPP piece / pieces Stück
PR pair / pairs Paar
E48 service unit / service units Leistungseinheit / Leistungseinheiten
LS lump sum / lump sums Pauschale / Pauschalen
MIN minute / minutes Minute / Minuten
HUR hour / hours Stunde / Stunden
DAY day / days Tag / Tage
WEE week / weeks Woche / Wochen
MON month / months Monat / Monate
ANN year / years Jahr / Jahre
GRM gram / grams Gramm
KGM kilogram / kilograms Kilogramm
TNE tonne / tonnes Tonne / Tonnen
MTR metre / metres Meter
KMT kilometre / kilometres Kilometer
MTK square metre / square metres Quadratmeter
MTQ cubic metre / cubic metres Kubikmeter
LTR litre / litres Liter
KWH kilowatt hour / kilowatt hours Kilowattstunde / Kilowattstunden

The English names are those of the Recommendations; C62, whose Recommendation 20 name is "one", takes its synonym "unit". A language without names of its own prints the English ones, as it does every other word. A code the table does not name prints as the code, and a unit given as a word, as documents written before the codes carry it ('hours'), prints as it is. The unit column is as wide as its widest name. unitName() on @design.estate/dees-document/shared returns what a position prints:

import { unitName } from '@design.estate/dees-document/shared';

unitName('DE', 'HUR', 2.5); // "Stunden"
unitName('EN', 'HUR', 1); // "hour"
unitName('DE', 'SET', 2); // "SET" — a valid code without a name here
unitName('DE', 'hours', 2); // "hours" — a word is printed as it is

Tax statements

  • Reverse charge: a document with reverseCharge: true prints "Steuerschuldnerschaft des Leistungsempfängers" in every language: the words § 14a Abs. 5 UStG requires for a domestic supply whose recipient owes the tax under § 13b UStG, and § 14a Abs. 1 UStG for a supply in another member state whose recipient owes the tax there, such as a business-to-business service under Art. 196 of the VAT Directive (2006/112/EC). A document in English adds the line "Reverse charge", the words of Art. 226 Nr. 11a of the VAT Directive.
  • VAT categories (@tsclass/tsclass 9.11): an item states its VAT category (vatCategory, EN 16931 BT-151) and the reason for an exemption (vatExemptionReason, BT-120). The sums print one VAT line per category and rate, named after the category for the ones other than standard rated and reverse charge ("MwSt. 0% – steuerfrei", "MwSt. 0% – Nullsteuersatz"; IGIC and IPSI are named instead of VAT). Below the sums every exemption prints its note (§ 14 Abs. 4 Satz 1 Nr. 8 UStG) with the positions it applies to: the item's reason, else for an exempt item, an intra-community supply, an export or an item not subject to VAT the words of its category ("Steuerfreie innergemeinschaftliche Lieferung (auf Positionen: 4)"). The VATEX code (vatExemptionReasonCode) is for e-invoices and is not printed. An item in reverse charge (vatCategory: 'AE') prints the reverse-charge words even when the document does not state reverseCharge. An item without a category prints as before.
  • Exemption reference as a note (§ 14 Abs. 4 Nr. 8 UStG): a host whose items state no category can still pass the reference as a note, for example 'Steuerfreie innergemeinschaftliche Lieferung nach § 4 Nr. 1 Buchst. b i. V. m. § 6a UStG.'; it prints below the totals like every note. Do not set reverseCharge for a tax-exempt intra-Community supply of goods (§ 4 Nr. 1 Buchst. b, § 6a UStG): it is exempt, not reverse charge (category K).

Paid amounts and advance payments

A document with a paidAmount other than zero (EN 16931 BT-113, from @tsclass/tsclass 9.9.0 on) prints below the gross total "Bereits gezahlt" / "Paid in advance" with that amount, and "Zu zahlen" / "Amount due" with the gross total less it (BT-115). The payment terms then name the amount due ("Zu zahlen: 238,00 € ohne Abzug bis zum …" / "Amount due: €238.00 without deduction until …"), and the QR pay box asks for it. When nothing is due, the document prints neither payment terms nor a QR pay box but "Es ist nichts zu zahlen: Der Gesamtbetrag ist bezahlt." / "Nothing is due: the total has been paid."; when more was paid than the total, the last sum is "Guthaben" / "Credit balance" and the sentence names it. A credit note and a self-billed invoice print the sums and ask for nothing, as before.

A final invoice (invoice, corrected-invoice or self-billed-invoice) that lists advancePayments deducts them as Abschnitt 14.8 Abs. 7 Satz 3 UStAE allows: the gross total received is deducted as the paid amount, and the tax it contains is stated per rate ("darin enthaltene USt 19%" / "of which VAT 19%"), added up from the vat of every group of every payment. Below the sums, one line per payment names the advance invoice by number and date and the day the payment was received, as far as the payment states them ("Anzahlung zur Rechnung Nr. AR-2025-0001 vom 01.02.25, eingegangen am 05.02.25").

paidAmount has to equal the gross sum of the advance payments, the net plus the vat of every group of every payment, to the last decimal; the sums are added in exact decimals. A document that breaks this is refused, not recomputed: PdfService.createPdfFromLetterObject() rejects and dedocument-dedocument renders nothing. assertPaidAmountMatchesAdvancePayments() and getPaidAmountSummary() on @design.estate/dees-document/shared run the check and return what is printed.

A final invoice after advance invoices that stated tax has to list those payments in advancePayments: with only a paidAmount, the document deducts the amount but states no tax on it, and the tax the advance invoices stated would be owed again (§ 14c Abs. 1 UStG; Abschnitt 14.8 Abs. 10 UStAE). The renderer cannot tell this case; the application that builds the document has to.

QR pay box

A document that asks for payment prints a SEPA credit transfer QR code (EPC QR code, EPC069-12) for its gross total, or for its amount due when it states a paid amount, to the sender's IBAN, with the document number as the remittance information (the free-text field, not the creditor-reference field, which only takes an RF reference). The code carries euros only, so the box is omitted for another currency, for a total below 0.01 or above 999,999,999.99, and when the sender has no IBAN. Without a BIC the code uses version 002, which makes the BIC optional. Nothing follows the last filled line, and a line break inside the name or the remittance information becomes a space. The name is cut to the code's 70 characters and the remittance information to 140, at whole grapheme clusters. The code holds at most 331 bytes: text of multi-byte characters that would exceed them cuts the remittance information further; when no remittance information would be left, or the other lines alone exceed 331 bytes, no code is built and the box is omitted. buildEpcQrPayload() on @design.estate/dees-document/shared returns the code's content, or null when no valid code exists.

Payment Reminders

A payment reminder (finance.TPaymentReminder, letter type 'payment-reminder', from @tsclass/tsclass 9.7.0 on) is a letter like any other, so it goes into PdfService.createPdfFromLetterObject(), <dedocument-dedocument> and <dedocument-viewer> as it is. It is no accounting document: it has no positions and no VAT. It prints what its data states and computes nothing beyond the sums and the deadline:

  • Letterhead: the reminder number (id) and, when set, the reminder level (reminderLevel).
  • Intro: topText when the reminder has one, otherwise a standard sentence.
  • Ground of the claim: one row per claim with the kind (invoice, corrected invoice or debit note) and number of the reminded document, its issue date, its due date when it stated one, its gross total and the amount still open.
  • Charges, when there are any: each with its description and amount and what it rests on. Default interest prints its computation as stated: principal, rate a year, base rate plus the surcharge points with the provision (§ 288 Abs. 1 or Abs. 2 BGB; on an obligation that arose from 1 January 2002 to 28 July 2014 the 8 points of § 288 Abs. 2 BGB a. F. with Art. 229 § 34 Satz 1 EGBGB; a continuing obligation that arose earlier comes under that version from 1 January 2003 (Art. 229 § 5 Satz 2 EGBGB), and its consideration rendered after 30 June 2016 under the current § 288 Abs. 2 BGB (Art. 229 § 34 Satz 2 EGBGB)), and the first and last day. The default lump sum prints § 288 Abs. 5 BGB. Every charge that accrues on a claim names it; interest and the lump sum always do. A cost with lump sums credited against it lists them (§ 288 Abs. 5 Satz 3 BGB); its amount is already the cost less them.
  • Sums: the open claims, the charges, and the total payable, their sum, added in cents. There are no VAT rows: the charges are damages, not consideration for a supply.
  • Notes, then the payment deadline, dueInDays after the reminder's date.
  • QR pay box for the total payable, with the reminder number as the remittance information, under the same conditions as on an invoice.
import { PdfService } from '@design.estate/dees-document';
import type { finance } from '@tsclass/tsclass';

const reminder: finance.TPaymentReminder = {
  ...letterEnvelope, // type 'payment-reminder', id, date, from, to, subject, ...
  type: 'payment-reminder',
  id: 'MA-2025-0007',
  reminderLevel: 2,
  claims: [
    { documentType: 'invoice', documentId: 'RE-2025-0042', issueDate, dueDate, totalGross: 1190, outstandingAmount: 1190 },
  ],
  charges: [
    { chargeType: 'default-lump-sum', description: 'Verzugspauschale', claimDocumentId: 'RE-2025-0042', amount: 40 },
  ],
  dueInDays: 14,
  currency: 'EUR',
  notes: [],
};

const pdfResult = await pdfService.createPdfFromLetterObject({
  letterData: reminder,
  documentSettings: { languageCode: 'DE' },
});

isPaymentReminder() and getPaymentReminderTotals() on @design.estate/dees-document/shared answer whether a letter is a reminder and return the sums it prints. When the reminder was sent and when it reached the debtor are not printed; the issuing application records them.

Draft vs Final Documents

Control document version display:

const invoice = {
  // ... other fields
  versionInfo: {
    type: 'draft', // Shows watermark "DRAFT" across pages
    version: '0.1.0',
  },
};

// For final documents
const invoice = {
  // ... other fields
  versionInfo: {
    type: 'final', // Clean document without watermark
    version: '1.0.0',
  },
};

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
A comprehensive tool for dynamically generating and rendering business documents like invoices using modern web technologies.
Readme
3.4 MiB
Languages
TypeScript 99.7%
HTML 0.3%