Compare commits
32 Commits
feat/optio
...
v3.18.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 468f8b51f6 | |||
| d4c4a2b9c1 | |||
| 1b617c7910 | |||
| 9676a270da | |||
| f3b4fac4c0 | |||
| a8c6c2625c | |||
| a2a1151ccb | |||
| daaed01d02 | |||
| e7d6c482ad | |||
| ed305f656c | |||
| 30e932a874 | |||
| 0a74e07288 | |||
| 4c3de8c942 | |||
| 49d44d9701 | |||
| 96a7acff33 | |||
| 65588686c7 | |||
| fcf9cc3fc9 | |||
| 20091143d2 | |||
| f708d0a2b2 | |||
| 7e9f4409f6 | |||
| 2e63af1aa2 | |||
| 5ebd2d508c | |||
| 9128fbb165 | |||
| 8473221c13 | |||
| e7554fa2cc | |||
| 7c1fabf58a | |||
| 1203b6cbd1 | |||
| 525f2906fb | |||
| b105382abf | |||
| b1cdec7d17 | |||
| f1d512b2e5 | |||
| db21b43120 |
@@ -2,37 +2,18 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
||||||
title: '[BUG] '
|
title: '[BUG] '
|
||||||
labels: bug
|
labels: Problem
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Beschreibung**
|
**Beschreibung**
|
||||||
Eine klare und prägnante Beschreibung des Fehlers.
|
|
||||||
|
|
||||||
**Reproduktion**
|
**Reproduktion**
|
||||||
Schritte, um den Fehler zu reproduzieren:
|
|
||||||
|
|
||||||
Entweder:
|
|
||||||
1. Gehe zu '...'
|
|
||||||
2. Klicke auf '...'
|
|
||||||
3. Scrolle runter zu '...'
|
|
||||||
4. Siehe Fehler
|
|
||||||
|
|
||||||
Oder Link zur Seite
|
|
||||||
|
|
||||||
**Erwartetes Verhalten**
|
|
||||||
Eine klare Beschreibung dessen, was du erwartet hast.
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
Falls zutreffend, füge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen.
|
|
||||||
|
|
||||||
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
||||||
|
|
||||||
**Umgebung:**
|
|
||||||
- Betriebssystem: [z.B. Windows, macOS, Linux]
|
|
||||||
- Browser / Version (falls relevant): [z.B. Chrome 120]
|
|
||||||
- Projekt-Version: [z.B. v1.0.2]
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Füge hier alle anderen Informationen zum Problem hinzu.
|
|
||||||
@@ -2,19 +2,16 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
about: Schlage eine Idee für dieses Projekt vor.
|
about: Schlage eine Idee für dieses Projekt vor.
|
||||||
title: '[FEATURE] '
|
title: '[FEATURE] '
|
||||||
labels: enhancement
|
labels: Funktionswunsch
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
||||||
Eine klare Beschreibung des Problems (z.B. "Ich bin immer genervt, wenn...").
|
|
||||||
|
|
||||||
**Lösungsvorschlag**
|
**Lösungsvorschlag**
|
||||||
Eine klare Beschreibung dessen, was du dir wünschst und wie es funktionieren soll.
|
|
||||||
|
|
||||||
**Alternativen**
|
**Alternativen**
|
||||||
Hast du über alternative Lösungen oder Workarounds nachgedacht?
|
|
||||||
|
|
||||||
**Zusätzlicher Kontext**
|
|
||||||
Hier ist Platz für weitere Informationen, Skizzen oder Beispiele von anderen Tools.
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"fill": "ts-node src/webdav/fill-file-sizes.ts",
|
"fill": "ts-node src/webdav/fill-file-sizes.ts",
|
||||||
"dev:dav": "tsx watch src/webdav/server.ts",
|
"dev:dav": "tsx watch src/webdav/server.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test:einvoice": "tsx --test tests/einvoice.test.ts tests/zugferdInvoice.test.ts tests/outgoingEInvoiceSettings.test.ts",
|
"test:einvoice": "tsx --test tests/einvoice.test.ts tests/zugferdInvoice.test.ts",
|
||||||
"start": "node dist/src/index.js",
|
"start": "node dist/src/index.js",
|
||||||
"schema:index": "ts-node scripts/generate-schema-index.ts",
|
"schema:index": "ts-node scripts/generate-schema-index.ts",
|
||||||
"bankcodes:update": "tsx scripts/generate-de-bank-codes.ts",
|
"bankcodes:update": "tsx scripts/generate-de-bank-codes.ts",
|
||||||
|
|||||||
@@ -30,18 +30,9 @@ export class EInvoiceRequirementsError extends Error {
|
|||||||
|
|
||||||
export const breakingIssues = (issues: ZugferdIssue[]) => issues.filter((issue) => issue.type === "breaking")
|
export const breakingIssues = (issues: ZugferdIssue[]) => issues.filter((issue) => issue.type === "breaking")
|
||||||
|
|
||||||
/** Ausgangs-E-Rechnungen sind pro Mandant ausdrücklich zu aktivieren. */
|
|
||||||
async function outgoingEInvoiceEnabled(server: FastifyInstance, tenantId: number) {
|
|
||||||
const [tenant] = await server.db.select({ businessInfo: tenants.businessInfo }).from(tenants)
|
|
||||||
.where(eq(tenants.id, tenantId)).limit(1)
|
|
||||||
if (!tenant) throw new Error("Mandant nicht gefunden")
|
|
||||||
return (tenant.businessInfo as any)?.outgoingEInvoiceEnabled === true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Prüft die Pflichtangaben eines (auch ungespeicherten) Belegs. Andere Belegarten liefern keine Hinweise. */
|
/** Prüft die Pflichtangaben eines (auch ungespeicherten) Belegs. Andere Belegarten liefern keine Hinweise. */
|
||||||
export async function checkOutgoingEInvoice(server: FastifyInstance, tenantId: number, document: any, options: { requireNumber?: boolean } = {}) {
|
export async function checkOutgoingEInvoice(server: FastifyInstance, tenantId: number, document: any, options: { requireNumber?: boolean } = {}) {
|
||||||
if (!document || !isZugferdDocumentType(document.type)) return [] as ZugferdIssue[]
|
if (!document || !isZugferdDocumentType(document.type)) return [] as ZugferdIssue[]
|
||||||
if (!await outgoingEInvoiceEnabled(server, tenantId)) return [] as ZugferdIssue[]
|
|
||||||
const context = await loadZugferdContext(server, tenantId, document)
|
const context = await loadZugferdContext(server, tenantId, document)
|
||||||
return checkZugferdRequirements(document, context, options)
|
return checkZugferdRequirements(document, context, options)
|
||||||
}
|
}
|
||||||
@@ -101,12 +92,11 @@ export async function loadZugferdContext(server: FastifyInstance, tenantId: numb
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bei aktivierter Ausgangs-E-Rechnung wird die PDF zur ZUGFeRD-Rechnung. Fehlende Pflichtangaben führen zu
|
* Macht aus der gerenderten Rechnungs-PDF eine ZUGFeRD-Rechnung. Fehlende Pflichtangaben führen zu
|
||||||
* EInvoiceRequirementsError, damit keine Rechnung ohne gültige E-Rechnung abgelegt wird.
|
* EInvoiceRequirementsError, damit keine Rechnung ohne gültige E-Rechnung abgelegt wird.
|
||||||
*/
|
*/
|
||||||
export async function attachZugferdToPdf(server: FastifyInstance, tenantId: number, document: any, pdf: Buffer) {
|
export async function attachZugferdToPdf(server: FastifyInstance, tenantId: number, document: any, pdf: Buffer) {
|
||||||
if (!isZugferdDocument(document)) return { pdf, issues: [] as ZugferdIssue[], einvoice: false }
|
if (!isZugferdDocument(document)) return { pdf, issues: [] as ZugferdIssue[], einvoice: false }
|
||||||
if (!await outgoingEInvoiceEnabled(server, tenantId)) return { pdf, issues: [] as ZugferdIssue[], einvoice: false }
|
|
||||||
const context = await loadZugferdContext(server, tenantId, document)
|
const context = await loadZugferdContext(server, tenantId, document)
|
||||||
const { xml, issues } = buildZugferdInvoiceXml(document, context)
|
const { xml, issues } = buildZugferdInvoiceXml(document, context)
|
||||||
const blocking = breakingIssues(issues)
|
const blocking = breakingIssues(issues)
|
||||||
|
|||||||
@@ -278,11 +278,6 @@ export default async function tenantRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
if (!data) return reply.code(400).send({ error: "data required" })
|
if (!data) return reply.code(400).send({ error: "data required" })
|
||||||
|
|
||||||
if (data.businessInfo && Object.prototype.hasOwnProperty.call(data.businessInfo, "outgoingEInvoiceEnabled")
|
|
||||||
&& typeof data.businessInfo.outgoingEInvoiceEnabled !== "boolean") {
|
|
||||||
return reply.code(400).send({ error: "Die Einstellung für Ausgangs-E-Rechnungen muss ein Wahrheitswert sein." })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data, "portalConfig")) {
|
if (Object.prototype.hasOwnProperty.call(data, "portalConfig")) {
|
||||||
const config = data.portalConfig
|
const config = data.portalConfig
|
||||||
if (!config || typeof config !== "object" || Array.isArray(config)
|
if (!config || typeof config !== "object" || Array.isArray(config)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import assert from "node:assert/strict"
|
|
||||||
import test from "node:test"
|
|
||||||
import { tenants } from "../db/schema"
|
|
||||||
import { PgDialect } from "drizzle-orm/pg-core"
|
|
||||||
import { assertOutgoingEInvoiceRequirements, attachZugferdToPdf, checkOutgoingEInvoice, EInvoiceRequirementsError } from "../src/modules/einvoice/outgoingZugferd.service"
|
|
||||||
|
|
||||||
const invoice = { type: "invoices", documentNumber: "RE-1", payment_type: "sepa", rows: [] }
|
|
||||||
function serverFor(businessInfo: any) {
|
|
||||||
const tenantIds: number[] = []
|
|
||||||
let otherReads = 0
|
|
||||||
const server = { db: { select: () => ({ from: (table: any) => {
|
|
||||||
const rows = table === tenants ? [{ businessInfo }] : []
|
|
||||||
if (table !== tenants) otherReads++
|
|
||||||
const query: any = {
|
|
||||||
where: (condition: any) => {
|
|
||||||
if (table === tenants) tenantIds.push(new PgDialect().sqlToQuery(condition).params[0] as number)
|
|
||||||
return query
|
|
||||||
},
|
|
||||||
limit: async () => rows,
|
|
||||||
then: (resolve: any, reject: any) => Promise.resolve(rows).then(resolve, reject),
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
} }) } } as any
|
|
||||||
return { server, tenantIds, otherReads: () => otherReads }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const businessInfo of [undefined, {}, { outgoingEInvoiceEnabled: false }, { outgoingEInvoiceEnabled: "true" }]) {
|
|
||||||
test(`disabled/legacy tenant skips e-invoice requirements: ${JSON.stringify(businessInfo)}`, async () => {
|
|
||||||
const { server, tenantIds, otherReads } = serverFor(businessInfo)
|
|
||||||
assert.deepEqual(await checkOutgoingEInvoice(server, 8, invoice), [])
|
|
||||||
await assertOutgoingEInvoiceRequirements(server, 8, invoice)
|
|
||||||
const pdf = Buffer.from("original PDF bytes")
|
|
||||||
const result = await attachZugferdToPdf(server, 8, invoice, pdf)
|
|
||||||
assert.equal(result.pdf, pdf)
|
|
||||||
assert.equal(result.einvoice, false)
|
|
||||||
assert.deepEqual(result.issues, [])
|
|
||||||
assert.deepEqual(tenantIds, [8, 8, 8])
|
|
||||||
assert.equal(otherReads(), 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test("enabled tenant still blocks incomplete invoices before finalization and embedding", async () => {
|
|
||||||
const { server, tenantIds } = serverFor({ outgoingEInvoiceEnabled: true })
|
|
||||||
const issues = await checkOutgoingEInvoice(server, 9, invoice)
|
|
||||||
assert.ok(issues.some(issue => issue.type === "breaking"))
|
|
||||||
await assert.rejects(assertOutgoingEInvoiceRequirements(server, 9, invoice), EInvoiceRequirementsError)
|
|
||||||
await assert.rejects(attachZugferdToPdf(server, 9, invoice, Buffer.from("pdf")), EInvoiceRequirementsError)
|
|
||||||
assert.ok(tenantIds.every(id => id === 9))
|
|
||||||
})
|
|
||||||
|
|
||||||
test("document payload cannot disable the tenant setting", async () => {
|
|
||||||
const { server } = serverFor({ outgoingEInvoiceEnabled: true })
|
|
||||||
await assert.rejects(assertOutgoingEInvoiceRequirements(server, 8, {
|
|
||||||
...invoice, businessInfo: { outgoingEInvoiceEnabled: false }, outgoingEInvoiceEnabled: false,
|
|
||||||
}), EInvoiceRequirementsError)
|
|
||||||
})
|
|
||||||
@@ -233,7 +233,6 @@ const activeTelephonyTargetOptions = computed(() => {
|
|||||||
return telephonyExtensionOptions.value.users || []
|
return telephonyExtensionOptions.value.users || []
|
||||||
})
|
})
|
||||||
const defaultBusinessInfo = {
|
const defaultBusinessInfo = {
|
||||||
outgoingEInvoiceEnabled: false,
|
|
||||||
name: "",
|
name: "",
|
||||||
street: "",
|
street: "",
|
||||||
zip: "",
|
zip: "",
|
||||||
@@ -753,13 +752,6 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<UInput v-model="businessInfo.bic"/>
|
<UInput v-model="businessInfo.bic"/>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField
|
|
||||||
label="Ausgangs-E-Rechnungen"
|
|
||||||
description="Optional für diesen Mandanten: Bei Aktivierung werden Rechnungs-PDFs mit ZUGFeRD-Daten erstellt und die erforderlichen Angaben geprüft. Deaktiviert werden normale PDFs ohne E-Rechnungsprüfung erstellt. Gilt auch für Serienrechnungen. Bereits gespeicherte Belege und der Empfang von E-Rechnungen bleiben unverändert."
|
|
||||||
class="mt-6"
|
|
||||||
>
|
|
||||||
<USwitch v-model="businessInfo.outgoingEInvoiceEnabled" label="E-Rechnungen erstellen" />
|
|
||||||
</UFormField>
|
|
||||||
<UButton
|
<UButton
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
@click="updateTenant({businessInfo: businessInfo})"
|
@click="updateTenant({businessInfo: businessInfo})"
|
||||||
|
|||||||
Reference in New Issue
Block a user