KI-AGENT: Skript zum Löschen von Rechnungsbereichen ergänzen
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"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",
|
||||||
|
"invoices:delete-range": "tsx scripts/delete-createddocument-range.ts",
|
||||||
"members:import:csv": "tsx scripts/import-members-csv.ts",
|
"members:import:csv": "tsx scripts/import-members-csv.ts",
|
||||||
"profiles:import:mitarbeiterliste": "tsx scripts/import-mitarbeiterliste.ts",
|
"profiles:import:mitarbeiterliste": "tsx scripts/import-mitarbeiterliste.ts",
|
||||||
"accounts:import:skr42": "ts-node scripts/import-skr42-accounts.ts"
|
"accounts:import:skr42": "ts-node scripts/import-skr42-accounts.ts"
|
||||||
|
|||||||
359
backend/scripts/delete-createddocument-range.ts
Normal file
359
backend/scripts/delete-createddocument-range.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import "dotenv/config"
|
||||||
|
import { Pool, type PoolClient } from "pg"
|
||||||
|
|
||||||
|
import { loadSecrets, secrets } from "../src/utils/secrets"
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
tenantId?: number
|
||||||
|
fromNumber?: string
|
||||||
|
toNumber?: string
|
||||||
|
fromId?: number
|
||||||
|
toId?: number
|
||||||
|
types: string[]
|
||||||
|
execute: boolean
|
||||||
|
unlinkRelated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetDocument = {
|
||||||
|
id: number
|
||||||
|
documentNumber: string | null
|
||||||
|
type: string
|
||||||
|
title: string | null
|
||||||
|
state: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TYPES = ["invoices"]
|
||||||
|
|
||||||
|
function printUsage() {
|
||||||
|
console.log(`
|
||||||
|
Löscht einen Bereich von Ausgangsbelegen und deren direkt verknüpfte DB-Dokumente.
|
||||||
|
|
||||||
|
Standardmäßig läuft das Skript als Dry-Run. Erst --execute löscht wirklich.
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
npm run invoices:delete-range -- --tenant 41 --from RE-2026-001 --to RE-2026-010
|
||||||
|
npm run invoices:delete-range -- --tenant 41 --from-id 1200 --to-id 1210 --execute
|
||||||
|
npm run invoices:delete-range -- --tenant 41 --from RE-2026-001 --to RE-2026-010 --type invoices --type advanceInvoices --execute
|
||||||
|
|
||||||
|
Optionen:
|
||||||
|
--tenant <id> Pflicht: Tenant-ID
|
||||||
|
--from <nummer> Erste Rechnungsnummer im Bereich
|
||||||
|
--to <nummer> Letzte Rechnungsnummer im Bereich
|
||||||
|
--from-id <id> Erste Beleg-ID im Bereich
|
||||||
|
--to-id <id> Letzte Beleg-ID im Bereich
|
||||||
|
--type <type> Belegtyp, mehrfach möglich. Standard: invoices
|
||||||
|
--execute Wirklich löschen. Ohne diese Option nur Vorschau.
|
||||||
|
--unlink-related Externe Belege, die auf Zielbelege zeigen, werden entkoppelt.
|
||||||
|
--help Hilfe anzeigen
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readValue(args: string[], index: number, flag: string) {
|
||||||
|
const value = args[index + 1]
|
||||||
|
if (!value || value.startsWith("--")) {
|
||||||
|
throw new Error(`Für ${flag} fehlt ein Wert.`)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumber(args: string[], index: number, flag: string) {
|
||||||
|
const value = Number(readValue(args, index, flag))
|
||||||
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||||
|
throw new Error(`${flag} muss eine positive Ganzzahl sein.`)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv: string[]): Args {
|
||||||
|
const parsed: Args = {
|
||||||
|
types: [],
|
||||||
|
execute: false,
|
||||||
|
unlinkRelated: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const arg = argv[i]
|
||||||
|
|
||||||
|
if (arg === "--help" || arg === "-h") {
|
||||||
|
printUsage()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--tenant") {
|
||||||
|
parsed.tenantId = readNumber(argv, i, arg)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--from") {
|
||||||
|
parsed.fromNumber = readValue(argv, i, arg)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--to") {
|
||||||
|
parsed.toNumber = readValue(argv, i, arg)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--from-id") {
|
||||||
|
parsed.fromId = readNumber(argv, i, arg)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--to-id") {
|
||||||
|
parsed.toId = readNumber(argv, i, arg)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--type") {
|
||||||
|
parsed.types.push(readValue(argv, i, arg))
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--execute") {
|
||||||
|
parsed.execute = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === "--unlink-related") {
|
||||||
|
parsed.unlinkRelated = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unbekannte Option: ${arg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.tenantId) throw new Error("--tenant ist Pflicht.")
|
||||||
|
|
||||||
|
const hasNumberRange = parsed.fromNumber !== undefined || parsed.toNumber !== undefined
|
||||||
|
const hasIdRange = parsed.fromId !== undefined || parsed.toId !== undefined
|
||||||
|
|
||||||
|
if (hasNumberRange && hasIdRange) {
|
||||||
|
throw new Error("Bitte entweder Rechnungsnummernbereich oder ID-Bereich verwenden, nicht beides.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNumberRange && (!parsed.fromNumber || !parsed.toNumber)) {
|
||||||
|
throw new Error("Für einen Rechnungsnummernbereich sind --from und --to Pflicht.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasIdRange && (!parsed.fromId || !parsed.toId)) {
|
||||||
|
throw new Error("Für einen ID-Bereich sind --from-id und --to-id Pflicht.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNumberRange && !hasIdRange) {
|
||||||
|
throw new Error("Bitte einen Bereich mit --from/--to oder --from-id/--to-id angeben.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.fromNumber && parsed.toNumber && parsed.fromNumber > parsed.toNumber) {
|
||||||
|
throw new Error("--from darf bei Textvergleich nicht größer als --to sein.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.fromId && parsed.toId && parsed.fromId > parsed.toId) {
|
||||||
|
throw new Error("--from-id darf nicht größer als --to-id sein.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.types.length === 0) parsed.types = DEFAULT_TYPES
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConnectionString() {
|
||||||
|
if (process.env.DATABASE_URL) return process.env.DATABASE_URL
|
||||||
|
|
||||||
|
await loadSecrets()
|
||||||
|
if (secrets.DATABASE_URL) return secrets.DATABASE_URL
|
||||||
|
|
||||||
|
throw new Error("DATABASE_URL ist nicht konfiguriert.")
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTargetWhere(args: Args, startParam = 1) {
|
||||||
|
const values: unknown[] = [args.tenantId, args.types]
|
||||||
|
const clauses = [
|
||||||
|
`"tenant" = $${startParam}`,
|
||||||
|
`"type" = ANY($${startParam + 1}::text[])`,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (args.fromNumber && args.toNumber) {
|
||||||
|
values.push(args.fromNumber, args.toNumber)
|
||||||
|
clauses.push(`"documentNumber" >= $${startParam + 2}`)
|
||||||
|
clauses.push(`"documentNumber" <= $${startParam + 3}`)
|
||||||
|
} else {
|
||||||
|
values.push(args.fromId, args.toId)
|
||||||
|
clauses.push(`"id" >= $${startParam + 2}`)
|
||||||
|
clauses.push(`"id" <= $${startParam + 3}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
whereSql: clauses.join(" AND "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectTargetDocuments(client: PoolClient, args: Args) {
|
||||||
|
const target = buildTargetWhere(args)
|
||||||
|
const result = await client.query<TargetDocument>(
|
||||||
|
`
|
||||||
|
SELECT "id", "documentNumber", "type", "title", "state"
|
||||||
|
FROM "createddocuments"
|
||||||
|
WHERE ${target.whereSql}
|
||||||
|
ORDER BY "id"
|
||||||
|
`,
|
||||||
|
target.values
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
async function countByTarget(client: PoolClient, table: string, column: string, targetIds: number[]) {
|
||||||
|
const result = await client.query<{ count: string }>(
|
||||||
|
`SELECT COUNT(*)::int AS "count" FROM "${table}" WHERE "${column}" = ANY($1::bigint[])`,
|
||||||
|
[targetIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Number(result.rows[0]?.count || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findExternalLinkedDocuments(client: PoolClient, targetIds: number[]) {
|
||||||
|
const result = await client.query<TargetDocument>(
|
||||||
|
`
|
||||||
|
SELECT "id", "documentNumber", "type", "title", "state"
|
||||||
|
FROM "createddocuments"
|
||||||
|
WHERE "linkedDocument" = ANY($1::bigint[])
|
||||||
|
AND NOT ("id" = ANY($1::bigint[]))
|
||||||
|
ORDER BY "id"
|
||||||
|
`,
|
||||||
|
[targetIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteByTarget(client: PoolClient, table: string, column: string, targetIds: number[]) {
|
||||||
|
const result = await client.query(
|
||||||
|
`DELETE FROM "${table}" WHERE "${column}" = ANY($1::bigint[])`,
|
||||||
|
[targetIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.rowCount || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function printTargets(targets: TargetDocument[]) {
|
||||||
|
console.table(
|
||||||
|
targets.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
nummer: item.documentNumber,
|
||||||
|
typ: item.type,
|
||||||
|
status: item.state,
|
||||||
|
titel: item.title,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const connectionString = await loadConnectionString()
|
||||||
|
const pool = new Pool({ connectionString, max: 1 })
|
||||||
|
const client = await pool.connect()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.query("BEGIN")
|
||||||
|
|
||||||
|
const targets = await selectTargetDocuments(client, args)
|
||||||
|
if (targets.length === 0) {
|
||||||
|
console.log("Keine passenden Rechnungen gefunden.")
|
||||||
|
await client.query("ROLLBACK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetIds = targets.map((item) => item.id)
|
||||||
|
const [fileCount, historyCount, allocationCount, externalLinks] = await Promise.all([
|
||||||
|
countByTarget(client, "files", "createddocument", targetIds),
|
||||||
|
countByTarget(client, "historyitems", "createddocument", targetIds),
|
||||||
|
countByTarget(client, "statementallocations", "cd_id", targetIds),
|
||||||
|
findExternalLinkedDocuments(client, targetIds),
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log(`Tenant: ${args.tenantId}`)
|
||||||
|
console.log(`Belegtypen: ${args.types.join(", ")}`)
|
||||||
|
console.log(`Modus: ${args.execute ? "Löschen" : "Dry-Run"}`)
|
||||||
|
console.log("")
|
||||||
|
printTargets(targets)
|
||||||
|
console.log("")
|
||||||
|
console.log("Betroffene verknüpfte DB-Zeilen:")
|
||||||
|
console.log(`- Dateien: ${fileCount}`)
|
||||||
|
console.log(`- Historie: ${historyCount}`)
|
||||||
|
console.log(`- Bank-Zuordnungen: ${allocationCount}`)
|
||||||
|
|
||||||
|
if (externalLinks.length > 0 && !args.unlinkRelated) {
|
||||||
|
console.log("")
|
||||||
|
console.log("Abbruch: Andere Belege verweisen auf Zielbelege.")
|
||||||
|
console.log("Nutze --unlink-related, wenn diese Verknüpfungen vor dem Löschen entfernt werden sollen.")
|
||||||
|
printTargets(externalLinks)
|
||||||
|
await client.query("ROLLBACK")
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.execute) {
|
||||||
|
console.log("")
|
||||||
|
console.log("Dry-Run abgeschlossen. Es wurde nichts gelöscht. Zum Löschen --execute ergänzen.")
|
||||||
|
await client.query("ROLLBACK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (externalLinks.length > 0) {
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE "createddocuments"
|
||||||
|
SET "linkedDocument" = NULL
|
||||||
|
WHERE "linkedDocument" = ANY($1::bigint[])
|
||||||
|
AND NOT ("id" = ANY($1::bigint[]))
|
||||||
|
`,
|
||||||
|
[targetIds]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`UPDATE "createddocuments" SET "linkedDocument" = NULL WHERE "id" = ANY($1::bigint[])`,
|
||||||
|
[targetIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
const deletedFiles = await deleteByTarget(client, "files", "createddocument", targetIds)
|
||||||
|
const deletedHistory = await deleteByTarget(client, "historyitems", "createddocument", targetIds)
|
||||||
|
const deletedAllocations = await deleteByTarget(client, "statementallocations", "cd_id", targetIds)
|
||||||
|
|
||||||
|
const deletedDocuments = await client.query(
|
||||||
|
`DELETE FROM "createddocuments" WHERE "id" = ANY($1::bigint[])`,
|
||||||
|
[targetIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.query("COMMIT")
|
||||||
|
|
||||||
|
console.log("")
|
||||||
|
console.log("Löschung abgeschlossen:")
|
||||||
|
console.log(`- Rechnungen: ${deletedDocuments.rowCount || 0}`)
|
||||||
|
console.log(`- Dateien: ${deletedFiles}`)
|
||||||
|
console.log(`- Historie: ${deletedHistory}`)
|
||||||
|
console.log(`- Bank-Zuordnungen: ${deletedAllocations}`)
|
||||||
|
if (externalLinks.length > 0) {
|
||||||
|
console.log(`- Entkoppelte externe Belege: ${externalLinks.length}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await client.query("ROLLBACK")
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
client.release()
|
||||||
|
await pool.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error("Fehler beim Löschen der Rechnungen:")
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user