Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement xrechnung invoice generation #8013

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 0 additions & 10 deletions resources/pdf/metadata.xml

This file was deleted.

17 changes: 17 additions & 0 deletions src/common/api/worker/facades/lazy/CustomerFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,23 @@ export class CustomerFacade {
}
}

async generateXRechnungInvoice(invoiceNumber: string): Promise<DataFile> {
const customer = await this.entityClient.load(CustomerTypeRef, assertNotNull(this.userFacade.getUser()?.customer))
const customerInfo = await this.entityClient.load(CustomerInfoTypeRef, customer.customerInfo)
const invoiceData = await this.serviceExecutor.get(InvoiceDataService, createInvoiceDataGetIn({ invoiceNumber }))
const { XRechnungInvoiceGenerator } = await import("../../invoicegen/XRechnungInvoiceGenerator.js")
const xRechnungGenerator = new XRechnungInvoiceGenerator(invoiceData, invoiceNumber, this.getCustomerId(), customerInfo.registrationMailAddress)
const xRechnungFile = xRechnungGenerator.generate()
return {
_type: "DataFile",
name: String(invoiceNumber) + ".xml",
mimeType: "application/xml",
data: xRechnungFile,
size: xRechnungFile.byteLength,
id: undefined,
}
}

async loadAccountingInfo(): Promise<AccountingInfo> {
const customer = await this.entityClient.load(CustomerTypeRef, assertNotNull(this.userFacade.getUser()?.customer))
const customerInfo = await this.entityClient.load(CustomerInfoTypeRef, customer.customerInfo)
Expand Down
123 changes: 123 additions & 0 deletions src/common/api/worker/invoicegen/InvoiceUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import InvoiceTexts from "./InvoiceTexts.js"

export const enum VatType {
NO_VAT = "0",
ADD_VAT = "1",
VAT_INCLUDED_SHOWN = "2",
VAT_INCLUDED_HIDDEN = "3",
NO_VAT_REVERSE_CHARGE = "4",
}

export const enum InvoiceType {
INVOICE = "0",
CREDIT = "1",
REFERRAL_CREDIT = "2",
}

export const enum PaymentMethod {
INVOICE = "0",
CREDIT_CARD = "1",
SEPA_UNUSED = "2",
PAYPAL = "3",
ACCOUNT_BALANCE = "4",
}

export const enum InvoiceItemType {
PREMIUM_USER = "0",
StarterUser = "1",
StarterUserPackage = "2",
StarterUserPackageUpgrade = "3",
StoragePackage = "4",
StoragePackageUpgrade = "5",
EmailAliasPackage = "6",
EmailAliasPackageUpgrade = "7",
SharedMailGroup = "8",
WhitelabelFeature = "9",
ContactForm_UNUSED = "10",
WhitelabelChild = "11",
LocalAdminGroup = "12",
Discount = "13",
SharingFeature = "14",
Credit = "15",
GiftCard = "16",
BusinessFeature = "17",
GiftCardMigration = "18",
ReferralCredit = "19",
CancelledReferralCredit = "20",
RevolutionaryAccount = "21",
LegendAccount = "22",
EssentialAccount = "23",
AdvancedAccount = "24",
UnlimitedAccount = "25",
}

/**
* Returns the language code of country as either "en" or "de"
* "de" is only returned if the country is Germany or Austria
* @param country
*/
export function countryUsesGerman(country: string): "de" | "en" {
return country === "DE" || country === "AT" ? "de" : "en"
}

/**
* Get the name of a given InvoiceItemType
*/
export function getInvoiceItemTypeName(type: NumberString, languageCode: "en" | "de"): string {
switch (type) {
case InvoiceItemType.PREMIUM_USER:
return InvoiceTexts[languageCode].premiumUser
case InvoiceItemType.StarterUser:
return InvoiceTexts[languageCode].starterUser
case InvoiceItemType.StarterUserPackage:
return InvoiceTexts[languageCode].starterUserPackage
case InvoiceItemType.StarterUserPackageUpgrade:
return InvoiceTexts[languageCode].starterUserPackageUpgrade
case InvoiceItemType.StoragePackage:
return InvoiceTexts[languageCode].storagePackage
case InvoiceItemType.StoragePackageUpgrade:
return InvoiceTexts[languageCode].storagePackageUpgrade
case InvoiceItemType.EmailAliasPackage:
return InvoiceTexts[languageCode].emailAliasPackage
case InvoiceItemType.EmailAliasPackageUpgrade:
return InvoiceTexts[languageCode].emailAliasPackageUpgrade
case InvoiceItemType.SharedMailGroup:
return InvoiceTexts[languageCode].sharedMailGroup
case InvoiceItemType.WhitelabelFeature:
return InvoiceTexts[languageCode].whitelabelFeature
case InvoiceItemType.ContactForm_UNUSED:
return InvoiceTexts[languageCode].contactFormUnused
case InvoiceItemType.WhitelabelChild:
return InvoiceTexts[languageCode].whitelabelChild
case InvoiceItemType.LocalAdminGroup:
return InvoiceTexts[languageCode].localAdminGroup
case InvoiceItemType.Discount:
return InvoiceTexts[languageCode].discount
case InvoiceItemType.SharingFeature:
return InvoiceTexts[languageCode].sharingFeature
case InvoiceItemType.Credit:
return InvoiceTexts[languageCode].creditType
case InvoiceItemType.GiftCard:
return InvoiceTexts[languageCode].giftCard
case InvoiceItemType.BusinessFeature:
return InvoiceTexts[languageCode].businessFeature
case InvoiceItemType.GiftCardMigration:
return InvoiceTexts[languageCode].giftCardMigration
case InvoiceItemType.ReferralCredit:
return InvoiceTexts[languageCode].referralCredit
case InvoiceItemType.CancelledReferralCredit:
return InvoiceTexts[languageCode].cancelledReferralCredit
case InvoiceItemType.RevolutionaryAccount:
return InvoiceTexts[languageCode].revolutionaryAccount
case InvoiceItemType.LegendAccount:
return InvoiceTexts[languageCode].legendAccount
case InvoiceItemType.EssentialAccount:
return InvoiceTexts[languageCode].essentialAccount
case InvoiceItemType.AdvancedAccount:
return InvoiceTexts[languageCode].advancedAccount
case InvoiceItemType.UnlimitedAccount:
return InvoiceTexts[languageCode].unlimitedAccount
default:
throw new Error("Unknown InvoiceItemType " + type)
}
}
150 changes: 27 additions & 123 deletions src/common/api/worker/invoicegen/PdfInvoiceGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,19 @@ import { MARGIN_LEFT, MARGIN_TOP, PDF_FONTS, PDF_IMAGES, PdfDocument, TABLE_VERT
import InvoiceTexts from "./InvoiceTexts.js"
import { PdfWriter } from "../pdf/PdfWriter.js"
import { InvoiceDataGetOut } from "../../entities/sys/TypeRefs.js"

const enum VatType {
NO_VAT = "0",
ADD_VAT = "1",
VAT_INCLUDED_SHOWN = "2",
VAT_INCLUDED_HIDDEN = "3",
NO_VAT_REVERSE_CHARGE = "4",
}

const enum InvoiceType {
INVOICE = "0",
CREDIT = "1",
REFERRAL_CREDIT = "2",
}

const enum PaymentMethod {
INVOICE = "0",
CREDIT_CARD = "1",
SEPA_UNUSED = "2",
PAYPAL = "3",
ACCOUNT_BALANCE = "4",
}

const enum InvoiceItemType {
PREMIUM_USER = "0",
StarterUser = "1",
StarterUserPackage = "2",
StarterUserPackageUpgrade = "3",
StoragePackage = "4",
StoragePackageUpgrade = "5",
EmailAliasPackage = "6",
EmailAliasPackageUpgrade = "7",
SharedMailGroup = "8",
WhitelabelFeature = "9",
ContactForm_UNUSED = "10",
WhitelabelChild = "11",
LocalAdminGroup = "12",
Discount = "13",
SharingFeature = "14",
Credit = "15",
GiftCard = "16",
BusinessFeature = "17",
GiftCardMigration = "18",
ReferralCredit = "19",
CancelledReferralCredit = "20",
RevolutionaryAccount = "21",
LegendAccount = "22",
EssentialAccount = "23",
AdvancedAccount = "24",
UnlimitedAccount = "25",
}
import { countryUsesGerman, getInvoiceItemTypeName, InvoiceItemType, InvoiceType, PaymentMethod, VatType } from "./InvoiceUtils.js"

/**
* Object generating a PDF invoice document.
* This document is ONLY responsible for rendering the data it gets and formatting it in a way that does not change anything about it.
* This generator is ONLY responsible for rendering the data it gets and formatting it in a way that does not change anything about it.
* If adjustments to the data must be made prior to rendering, then these should take place within the RenderInvoice service.
*/
export class PdfInvoiceGenerator {
private readonly doc: PdfDocument
private readonly languageCode: "de" | "en" = "en"
private readonly invoiceNumber: string
private readonly customerId: string
private invoice: InvoiceDataGetOut
private invoiceNumber: string
private customerId: string

constructor(pdfWriter: PdfWriter, invoice: InvoiceDataGetOut, invoiceNumber: string, customerId: string) {
this.invoice = invoice
Expand Down Expand Up @@ -170,14 +120,14 @@ export class PdfInvoiceGenerator {
// Entry with all invoice info
tableData.push([
this.formatAmount(invoiceItem.itemType, invoiceItem.amount),
this.getInvoiceItemTypeName(invoiceItem.itemType),
getInvoiceItemTypeName(invoiceItem.itemType, this.languageCode),
invoiceItem.singlePrice == null ? "" : this.formatInvoiceCurrency(invoiceItem.singlePrice),
this.formatInvoiceCurrency(invoiceItem.totalPrice),
])
// Entry with date range
tableData.push(["", `${this.formatInvoiceDate(invoiceItem.startDate)} - ${this.formatInvoiceDate(invoiceItem.endDate)}`, "", ""])
}
const tableEndPoint = await this.doc.addTable([MARGIN_LEFT, MARGIN_TOP + 120], 165, columns, tableData)
const tableEndPoint = await this.doc.addTable([MARGIN_LEFT, MARGIN_TOP + 120], 165, columns, tableData, this.getTableRowsForFirstPage())

this.renderTableSummary(tableEndPoint, columns)
this.doc.changeTextCursorPosition([MARGIN_LEFT, tableEndPoint + 4 * TABLE_VERTICAL_SPACING])
Expand All @@ -190,6 +140,8 @@ export class PdfInvoiceGenerator {
// Line break that's to be removed if no VAT appears in the summary
let additionalVerticalSpace = 1

this.doc.changeFont(PDF_FONTS.REGULAR, 11)

// Sub total
this.doc.addTableRow([MARGIN_LEFT, tableEndPoint], columns, [
"",
Expand Down Expand Up @@ -231,7 +183,7 @@ export class PdfInvoiceGenerator {
* Additional blocks displayed below the table depending on invoice type, vat type and payment method
*/
renderAdditional() {
this.doc.changeFont(PDF_FONTS.REGULAR, 12)
this.doc.changeFont(PDF_FONTS.REGULAR, 11)

// No VAT / VAT not shown in table
switch (this.invoice.vatType) {
Expand All @@ -249,6 +201,7 @@ export class PdfInvoiceGenerator {
.addText(`${InvoiceTexts[this.languageCode].yourVatId} `)
.changeFont(PDF_FONTS.BOLD, 11)
.addText(`${this.invoice.vatIdNumber}`)
.changeFont(PDF_FONTS.REGULAR, 11)
} else {
this.doc.addText(InvoiceTexts[this.languageCode].netPricesNoVatInGermany)
}
Expand Down Expand Up @@ -320,6 +273,23 @@ export class PdfInvoiceGenerator {
.addText(InvoiceTexts[this.languageCode].legalBankAccount)
}

/**
* Determines how many table rows (invoice items) can be rendered on the first page depending on the texts that follow after the table
*/
getTableRowsForFirstPage(): number {
if (
this.invoice.paymentMethod === PaymentMethod.INVOICE &&
this.invoice.vatIdNumber != null &&
(this.invoice.vatType === VatType.NO_VAT || this.invoice.vatType === VatType.NO_VAT_REVERSE_CHARGE)
) {
// In these scenarios, there will be a lot of text after the table summary, so few rows can render
return 4
} else {
// In all other scenarios, there will be little text after the table summary, so more rows can render
return 8
}
}

/**
* Get the name of a given InvoiceType
*/
Expand All @@ -340,68 +310,6 @@ export class PdfInvoiceGenerator {
}
}

/**
* Get the name of a given InvoiceItemType
*/
getInvoiceItemTypeName(type: NumberString): string {
switch (type) {
case InvoiceItemType.PREMIUM_USER:
return InvoiceTexts[this.languageCode].premiumUser
case InvoiceItemType.StarterUser:
return InvoiceTexts[this.languageCode].starterUser
case InvoiceItemType.StarterUserPackage:
return InvoiceTexts[this.languageCode].starterUserPackage
case InvoiceItemType.StarterUserPackageUpgrade:
return InvoiceTexts[this.languageCode].starterUserPackageUpgrade
case InvoiceItemType.StoragePackage:
return InvoiceTexts[this.languageCode].storagePackage
case InvoiceItemType.StoragePackageUpgrade:
return InvoiceTexts[this.languageCode].storagePackageUpgrade
case InvoiceItemType.EmailAliasPackage:
return InvoiceTexts[this.languageCode].emailAliasPackage
case InvoiceItemType.EmailAliasPackageUpgrade:
return InvoiceTexts[this.languageCode].emailAliasPackageUpgrade
case InvoiceItemType.SharedMailGroup:
return InvoiceTexts[this.languageCode].sharedMailGroup
case InvoiceItemType.WhitelabelFeature:
return InvoiceTexts[this.languageCode].whitelabelFeature
case InvoiceItemType.ContactForm_UNUSED:
return InvoiceTexts[this.languageCode].contactFormUnused
case InvoiceItemType.WhitelabelChild:
return InvoiceTexts[this.languageCode].whitelabelChild
case InvoiceItemType.LocalAdminGroup:
return InvoiceTexts[this.languageCode].localAdminGroup
case InvoiceItemType.Discount:
return InvoiceTexts[this.languageCode].discount
case InvoiceItemType.SharingFeature:
return InvoiceTexts[this.languageCode].sharingFeature
case InvoiceItemType.Credit:
return InvoiceTexts[this.languageCode].creditType
case InvoiceItemType.GiftCard:
return InvoiceTexts[this.languageCode].giftCard
case InvoiceItemType.BusinessFeature:
return InvoiceTexts[this.languageCode].businessFeature
case InvoiceItemType.GiftCardMigration:
return InvoiceTexts[this.languageCode].giftCardMigration
case InvoiceItemType.ReferralCredit:
return InvoiceTexts[this.languageCode].referralCredit
case InvoiceItemType.CancelledReferralCredit:
return InvoiceTexts[this.languageCode].cancelledReferralCredit
case InvoiceItemType.RevolutionaryAccount:
return InvoiceTexts[this.languageCode].revolutionaryAccount
case InvoiceItemType.LegendAccount:
return InvoiceTexts[this.languageCode].legendAccount
case InvoiceItemType.EssentialAccount:
return InvoiceTexts[this.languageCode].essentialAccount
case InvoiceItemType.AdvancedAccount:
return InvoiceTexts[this.languageCode].advancedAccount
case InvoiceItemType.UnlimitedAccount:
return InvoiceTexts[this.languageCode].unlimitedAccount
default:
throw new Error("Unknown InvoiceItemType " + type)
}
}

/**
* Format the date depending on document language (dd.mm.yyyy) / (dd. Mon yyyy)
*/
Expand Down Expand Up @@ -442,7 +350,3 @@ export class PdfInvoiceGenerator {
}
}
}

function countryUsesGerman(country: string): "de" | "en" {
return country === "DE" || country === "AT" ? "de" : "en"
}
Loading
Loading