635 lines
21 KiB
TypeScript
635 lines
21 KiB
TypeScript
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { Repository } from 'typeorm';
|
||
import * as fs from 'fs/promises';
|
||
import * as path from 'path';
|
||
import * as crypto from 'crypto';
|
||
import { InboxPostprocessingAction } from '../database/entities/inbox-postprocessing-action.entity';
|
||
import { InboxDocument } from '../database/entities/inbox-document.entity';
|
||
import { BarcodeTemplate } from '../database/entities/barcode-template.entity';
|
||
import { Task } from '../database/entities/task.entity';
|
||
import { PageCacheService } from '../barcode/page-cache.service';
|
||
import { PaperlessService } from '../paperless/paperless.service';
|
||
import { MailService } from '../postprocessing/mail.service';
|
||
import { ExportService } from '../postprocessing/export.service';
|
||
import {
|
||
applyEditsToTemp,
|
||
cleanupTemp,
|
||
extractSectionToTemp,
|
||
} from './edit-applier';
|
||
import { applyTemplate, buildVariables } from './variable-resolver';
|
||
import { DokumentVersionService } from '../paperless/dokument-version.service';
|
||
import {
|
||
aktionenAbEntscheidung,
|
||
type PaperlessEntscheidung,
|
||
} from './paperless-entscheidung.util';
|
||
|
||
function parseFlexDate(s: string): Date | null {
|
||
if (!s) return null;
|
||
// ISO: YYYY-MM-DD
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
||
const d = new Date(s);
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
// German: DD.MM.YYYY
|
||
const m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||
if (m) {
|
||
const d = new Date(
|
||
`${m[3]}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}`,
|
||
);
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export interface ActionResult {
|
||
sectionIndex: number;
|
||
actionId: number;
|
||
actionType: string;
|
||
ok: boolean;
|
||
skipped?: boolean;
|
||
message?: string;
|
||
duplicateOfDocumentId?: number;
|
||
/** Duplikat mit genau einem Stammdokument → neue Version möglich */
|
||
versionMoeglich?: boolean;
|
||
/** Alle gefundenen Stammdokumente (bei mehrdeutigen Treffern) */
|
||
duplikatKandidaten?: number[];
|
||
/** Der Benutzer hat das Duplikat verworfen */
|
||
verworfen?: boolean;
|
||
}
|
||
|
||
export interface RunForDocumentResult {
|
||
results: ActionResult[];
|
||
totalSections: number;
|
||
}
|
||
|
||
interface PaperlessRunResult {
|
||
skipped?: boolean;
|
||
message?: string;
|
||
duplicateOfDocumentId?: number;
|
||
versionMoeglich?: boolean;
|
||
duplikatKandidaten?: number[];
|
||
}
|
||
|
||
@Injectable()
|
||
export class InboxPostprocessorService {
|
||
private readonly logger = new Logger(InboxPostprocessorService.name);
|
||
|
||
constructor(
|
||
private readonly pageCache: PageCacheService,
|
||
private readonly paperlessService: PaperlessService,
|
||
private readonly dokumentVersion: DokumentVersionService,
|
||
private readonly mailService: MailService,
|
||
private readonly exportService: ExportService,
|
||
@InjectRepository(InboxPostprocessingAction)
|
||
private readonly actionRepo: Repository<InboxPostprocessingAction>,
|
||
@InjectRepository(InboxDocument)
|
||
private readonly docRepo: Repository<InboxDocument>,
|
||
@InjectRepository(BarcodeTemplate)
|
||
private readonly templateRepo: Repository<BarcodeTemplate>,
|
||
@InjectRepository(Task)
|
||
private readonly taskRepo: Repository<Task>,
|
||
) {}
|
||
|
||
async runForDocument(
|
||
documentId: string,
|
||
preferredUsername: string | null,
|
||
sectionOffset: number = 0,
|
||
processOnlyOne: boolean = false,
|
||
entscheidung?: PaperlessEntscheidung,
|
||
): Promise<RunForDocumentResult> {
|
||
const doc = await this.docRepo.findOne({ where: { Id: documentId } });
|
||
if (!doc) throw new NotFoundException('Dokument nicht gefunden');
|
||
if (doc.Source === 'user' && doc.OwnerUsername !== preferredUsername) {
|
||
throw new NotFoundException('Dokument nicht gefunden');
|
||
}
|
||
|
||
const sourcePdf = this.pageCache.documentPdfPath(doc.Id);
|
||
try {
|
||
await fs.access(sourcePdf);
|
||
} catch {
|
||
throw new NotFoundException('Dokument-PDF fehlt');
|
||
}
|
||
|
||
const templates = await this.templateRepo.find({ order: { Id: 'ASC' } });
|
||
const matchedTemplateIds = [
|
||
...new Set(
|
||
doc.QrCodes.map((qr) => {
|
||
const tpl = templates.find((t) => {
|
||
try {
|
||
return new RegExp(t.Regex).test(qr.value);
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
return tpl?.Id ?? null;
|
||
}).filter((id): id is number => id !== null),
|
||
),
|
||
];
|
||
if (matchedTemplateIds.length === 0)
|
||
return { results: [], totalSections: 0 };
|
||
|
||
const actions = await this.actionRepo.find({
|
||
where: matchedTemplateIds.map((tid) => ({
|
||
BarcodeTemplateId: tid,
|
||
IsActive: true,
|
||
})),
|
||
order: { BarcodeTemplateId: 'ASC', Order: 'ASC', Id: 'ASC' },
|
||
});
|
||
|
||
if (actions.length === 0) {
|
||
return { results: [], totalSections: 0 };
|
||
}
|
||
|
||
// Aktionen nach Template gruppieren, damit jede Gruppe ihre eigenen Barcode-Variablen bekommt
|
||
const actionsByTemplate = new Map<number, InboxPostprocessingAction[]>();
|
||
for (const action of actions) {
|
||
const tid = action.BarcodeTemplateId!;
|
||
if (!actionsByTemplate.has(tid)) actionsByTemplate.set(tid, []);
|
||
actionsByTemplate.get(tid)!.push(action);
|
||
}
|
||
|
||
let processedPdfPath: string | null = null;
|
||
let abortProcessing = false;
|
||
const results: ActionResult[] = [];
|
||
try {
|
||
processedPdfPath = await applyEditsToTemp(doc, sourcePdf);
|
||
|
||
// Überlebende Seiten berechnen (1-basiert auf Original)
|
||
const deletedSet = new Set(doc.DeletedPages ?? []);
|
||
const survivingOriginalPages: number[] = [];
|
||
for (let p = 1; p <= doc.PageCount; p++) {
|
||
if (!deletedSet.has(p)) survivingOriginalPages.push(p);
|
||
}
|
||
// Mapping: Original-Seite → 0-basierter Index in processedPdf
|
||
const processedPageIndex = new Map<number, number>();
|
||
survivingOriginalPages.forEach((origPage, idx) =>
|
||
processedPageIndex.set(origPage, idx),
|
||
);
|
||
|
||
// QR-Codes mit ihrem Index in der verarbeiteten PDF (gelöschte QR-Seiten herausfiltern)
|
||
const qrsWithIdx = doc.QrCodes.filter((qr) =>
|
||
processedPageIndex.has(qr.page),
|
||
)
|
||
.map((qr) => ({
|
||
page: qr.page,
|
||
value: qr.value,
|
||
processedIdx: processedPageIndex.get(qr.page)!,
|
||
}))
|
||
.sort((a, b) => a.processedIdx - b.processedIdx);
|
||
|
||
// Split-Punkte: 0-basierte Indizes in der verarbeiteten PDF wo SplitBefore-Templates beginnen
|
||
const splitPoints: number[] = [];
|
||
for (const qr of qrsWithIdx) {
|
||
const tplMatch = templates.find((t) => {
|
||
try {
|
||
return new RegExp(t.Regex).test(qr.value);
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
if (tplMatch?.SplitBefore) splitPoints.push(qr.processedIdx);
|
||
}
|
||
const processedPageCount = survivingOriginalPages.length;
|
||
|
||
// Gesamtzahl aktiver Abschnitte vorab berechnen (für Stepper-UI im Frontend)
|
||
let totalSections = 0;
|
||
for (let i = 0; i < qrsWithIdx.length; i++) {
|
||
const qr = qrsWithIdx[i];
|
||
const tpl = templates.find((t) => {
|
||
try {
|
||
return new RegExp(t.Regex).test(qr.value);
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
if (!tpl) continue;
|
||
if (i > 0 && !tpl.SplitBefore) continue;
|
||
totalSections++;
|
||
}
|
||
|
||
// Ersten Barcode immer verarbeiten (SplitBefore egal).
|
||
// Weitere Barcodes nur wenn SplitBefore=true → neues Dokument.
|
||
// sectionOffset: erste N aktive Abschnitte überspringen (für "Nächstes Dokument").
|
||
// processOnlyOne: nach dem ersten verarbeiteten Abschnitt abbrechen (für Wizard).
|
||
let activeSectionCount = 0;
|
||
let processedSection = false;
|
||
for (let i = 0; i < qrsWithIdx.length; i++) {
|
||
if (abortProcessing) break;
|
||
if (processedSection && processOnlyOne) break;
|
||
const qr = qrsWithIdx[i];
|
||
|
||
const tpl = templates.find((t) => {
|
||
try {
|
||
return new RegExp(t.Regex).test(qr.value);
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
if (!tpl) continue;
|
||
if (i > 0 && !tpl.SplitBefore) continue;
|
||
|
||
if (activeSectionCount < sectionOffset) {
|
||
activeSectionCount++;
|
||
continue;
|
||
}
|
||
const currentSectionIndex = activeSectionCount;
|
||
activeSectionCount++;
|
||
|
||
const tplActions = actionsByTemplate.get(tpl.Id);
|
||
if (!tplActions || tplActions.length === 0) continue;
|
||
|
||
const variables = buildVariables({
|
||
doc,
|
||
template: tpl,
|
||
matchingQrValue: qr.value,
|
||
});
|
||
|
||
// Abschnitt aus der verarbeiteten PDF extrahieren
|
||
const startIdx = qr.processedIdx;
|
||
const nextSplitIdx = splitPoints.find((sp) => sp > startIdx);
|
||
const endIdx =
|
||
nextSplitIdx !== undefined
|
||
? nextSplitIdx - 1
|
||
: processedPageCount - 1;
|
||
const pageIndices = Array.from(
|
||
{ length: endIdx - startIdx + 1 },
|
||
(_, i) => startIdx + i,
|
||
);
|
||
const sectionPdfPath = await extractSectionToTemp(
|
||
processedPdfPath,
|
||
pageIndices,
|
||
);
|
||
|
||
const defaultFilenameBase = tpl.DateinameTemplate
|
||
? applyTemplate(tpl.DateinameTemplate, variables)
|
||
: undefined;
|
||
|
||
try {
|
||
for (const action of aktionenAbEntscheidung(
|
||
tplActions,
|
||
entscheidung,
|
||
)) {
|
||
if (abortProcessing) break;
|
||
if (
|
||
action.ActionType === 'PAPERLESS' &&
|
||
entscheidung?.aktion === 'verwerfen'
|
||
) {
|
||
results.push({
|
||
sectionIndex: currentSectionIndex,
|
||
actionId: action.Id,
|
||
actionType: action.ActionType,
|
||
ok: true,
|
||
verworfen: true,
|
||
message: 'Verworfen',
|
||
});
|
||
} else if (action.ActionType === 'PAPERLESS') {
|
||
try {
|
||
const res = await this.runPaperless(
|
||
action.Content ?? {},
|
||
sectionPdfPath,
|
||
variables,
|
||
defaultFilenameBase,
|
||
entscheidung,
|
||
);
|
||
results.push({
|
||
sectionIndex: currentSectionIndex,
|
||
actionId: action.Id,
|
||
actionType: action.ActionType,
|
||
ok: true,
|
||
skipped: res.skipped,
|
||
message: res.message,
|
||
duplicateOfDocumentId: res.duplicateOfDocumentId,
|
||
versionMoeglich: res.versionMoeglich,
|
||
duplikatKandidaten: res.duplikatKandidaten,
|
||
});
|
||
if (res.skipped) {
|
||
abortProcessing = true;
|
||
break;
|
||
}
|
||
} catch (err: any) {
|
||
this.logger.error(
|
||
`Aktion PAPERLESS#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${err.message}`,
|
||
);
|
||
results.push({
|
||
sectionIndex: currentSectionIndex,
|
||
actionId: action.Id,
|
||
actionType: action.ActionType,
|
||
ok: false,
|
||
message: err.message,
|
||
});
|
||
}
|
||
} else {
|
||
try {
|
||
await this.runAction(
|
||
action,
|
||
sectionPdfPath,
|
||
doc,
|
||
variables,
|
||
defaultFilenameBase,
|
||
);
|
||
results.push({
|
||
sectionIndex: currentSectionIndex,
|
||
actionId: action.Id,
|
||
actionType: action.ActionType,
|
||
ok: true,
|
||
});
|
||
} catch (err: any) {
|
||
this.logger.error(
|
||
`Aktion ${action.ActionType}#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${err.message}`,
|
||
);
|
||
results.push({
|
||
sectionIndex: currentSectionIndex,
|
||
actionId: action.Id,
|
||
actionType: action.ActionType,
|
||
ok: false,
|
||
message: err.message,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
await cleanupTemp(sectionPdfPath);
|
||
}
|
||
|
||
processedSection = true;
|
||
}
|
||
|
||
return { results, totalSections };
|
||
} finally {
|
||
await cleanupTemp(processedPdfPath);
|
||
}
|
||
}
|
||
|
||
private async runAction(
|
||
action: InboxPostprocessingAction,
|
||
pdfPath: string,
|
||
doc: InboxDocument,
|
||
variables: Record<string, string>,
|
||
defaultFilenameBase?: string,
|
||
): Promise<void> {
|
||
const content = action.Content ?? {};
|
||
|
||
switch (action.ActionType) {
|
||
case 'MAIL':
|
||
return this.runMail(
|
||
content,
|
||
pdfPath,
|
||
doc,
|
||
variables,
|
||
defaultFilenameBase,
|
||
);
|
||
case 'EXPORT':
|
||
return this.runExport(content, pdfPath, variables, defaultFilenameBase);
|
||
default:
|
||
throw new Error(`Unbekannter ActionType: ${action.ActionType}`);
|
||
}
|
||
}
|
||
|
||
private async runMail(
|
||
content: Record<string, any>,
|
||
pdfPath: string,
|
||
doc: InboxDocument,
|
||
variables: Record<string, string>,
|
||
defaultFilenameBase?: string,
|
||
): Promise<void> {
|
||
const to = applyTemplate(String(content.to ?? ''), variables).trim();
|
||
if (!to) throw new Error('Empfänger fehlt');
|
||
const subject = applyTemplate(String(content.subject ?? ''), variables);
|
||
const body = applyTemplate(String(content.body ?? ''), variables);
|
||
const filenameTpl = String(content.filenameTemplate ?? '').trim();
|
||
const filename = filenameTpl
|
||
? `${applyTemplate(filenameTpl, variables)}.pdf`
|
||
: defaultFilenameBase
|
||
? `${defaultFilenameBase}.pdf`
|
||
: doc.OriginalName;
|
||
const buffer = await fs.readFile(pdfPath);
|
||
await this.mailService.sendMail({
|
||
to,
|
||
subject,
|
||
body,
|
||
attachments: [{ filename, content: buffer }],
|
||
});
|
||
}
|
||
|
||
private async runExport(
|
||
content: Record<string, any>,
|
||
pdfPath: string,
|
||
variables: Record<string, string>,
|
||
defaultFilenameBase?: string,
|
||
): Promise<void> {
|
||
const targetId = Number(content.exportTargetId);
|
||
if (!targetId) throw new Error('Export-Ziel fehlt');
|
||
const filenameTpl = String(content.filenameTemplate ?? '').trim();
|
||
const filename = filenameTpl
|
||
? `${applyTemplate(filenameTpl, variables)}.pdf`
|
||
: defaultFilenameBase
|
||
? `${defaultFilenameBase}.pdf`
|
||
: `${path.basename(pdfPath, '.pdf')}.pdf`;
|
||
const buffer = await fs.readFile(pdfPath);
|
||
await this.exportService.exportFile(targetId, filename, buffer);
|
||
}
|
||
|
||
private async runPaperless(
|
||
content: Record<string, any>,
|
||
pdfPath: string,
|
||
variables: Record<string, string>,
|
||
defaultFilenameBase?: string,
|
||
entscheidung?: PaperlessEntscheidung,
|
||
): Promise<PaperlessRunResult> {
|
||
// 1. Interne Belegnummer auflösen (Pflicht)
|
||
const intNrTpl = String(content.interneBelegnummer ?? '').trim();
|
||
if (!intNrTpl)
|
||
throw new Error(
|
||
'Interne Belegnummer ist in der Aktion nicht konfiguriert',
|
||
);
|
||
const interneBelegnummer = applyTemplate(intNrTpl, variables).trim();
|
||
if (!interneBelegnummer)
|
||
throw new Error('Interne Belegnummer konnte nicht aufgelöst werden');
|
||
|
||
// 2. Vom Benutzer bestätigt: als neue Version an das Stammdokument hängen
|
||
if (entscheidung?.aktion === 'version') {
|
||
await this.dokumentVersion.haengeVersionAn(
|
||
entscheidung.documentId,
|
||
pdfPath,
|
||
);
|
||
this.logger.log(
|
||
`Belegnummer ${interneBelegnummer} als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
|
||
);
|
||
return {
|
||
message: `Als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
|
||
};
|
||
}
|
||
|
||
// 3. ASN bestimmen (für den Upload)
|
||
const asnTpl = String(content.asn ?? '').trim();
|
||
const asn = asnTpl ? applyTemplate(asnTpl, variables).trim() : null;
|
||
let archiveSerialNumber: number | undefined;
|
||
if (asn) {
|
||
const n = parseInt(asn.replace(/[^0-9]/g, ''), 10);
|
||
if (!Number.isNaN(n)) archiveSerialNumber = n;
|
||
}
|
||
if (archiveSerialNumber === undefined) {
|
||
const n = parseInt(interneBelegnummer.replace(/[^0-9]/g, ''), 10);
|
||
if (!Number.isNaN(n)) archiveSerialNumber = n;
|
||
}
|
||
|
||
// 4. Duplikat-Check Paperless: CF7 + aus der Belegnummer abgeleitete ASN
|
||
const pruefung =
|
||
await this.paperlessService.pruefeBelegnummer(interneBelegnummer);
|
||
if (pruefung.status !== 'frei') {
|
||
this.logger.warn(
|
||
`Duplikat (Paperless): Belegnummer ${interneBelegnummer} bereits vorhanden (Dokument(e) ${pruefung.kandidaten.join(', ')})`,
|
||
);
|
||
return {
|
||
skipped: true,
|
||
message:
|
||
pruefung.status === 'eindeutig'
|
||
? `Duplikat – Belegnummer ${interneBelegnummer} bereits in Paperless`
|
||
: `Duplikat – Belegnummer ${interneBelegnummer} mehrfach in Paperless`,
|
||
duplicateOfDocumentId: pruefung.documentId ?? pruefung.kandidaten[0],
|
||
versionMoeglich: pruefung.status === 'eindeutig',
|
||
duplikatKandidaten: pruefung.kandidaten,
|
||
};
|
||
}
|
||
|
||
// 5. Duplikat-Check lokal (tasks-Tabelle)
|
||
const existingTask = await this.taskRepo.findOneBy({
|
||
InterneBelegnummer: interneBelegnummer,
|
||
});
|
||
if (existingTask) {
|
||
this.logger.warn(
|
||
`Duplikat (lokal): Belegnummer ${interneBelegnummer} bereits in tasks-Tabelle`,
|
||
);
|
||
return {
|
||
skipped: true,
|
||
message: `Duplikat – Belegnummer ${interneBelegnummer} bereits vorhanden`,
|
||
duplicateOfDocumentId: existingTask.PaperlessDocumentID ?? undefined,
|
||
versionMoeglich: false,
|
||
};
|
||
}
|
||
|
||
// 6. Checksum berechnen und prüfen
|
||
const buffer = await fs.readFile(pdfPath);
|
||
// Paperless ≥ 3.0 vergleicht SHA-256-Checksummen
|
||
const checksum = crypto.createHash('sha256').update(buffer).digest('hex');
|
||
const checksumExists = await this.paperlessService.checksumExists(checksum);
|
||
if (checksumExists) {
|
||
this.logger.warn(`Duplikat (Checksum): ${checksum}`);
|
||
return {
|
||
skipped: true,
|
||
message: 'Duplikat (Checksum-Übereinstimmung)',
|
||
};
|
||
}
|
||
|
||
// 7. Restliche Metadaten auflösen
|
||
|
||
const titleTpl = String(content.title ?? '').trim();
|
||
const title = titleTpl
|
||
? applyTemplate(titleTpl, variables)
|
||
: defaultFilenameBase || undefined;
|
||
|
||
const tags = Array.isArray(content.tags)
|
||
? content.tags
|
||
.map((t: any) => Number(t))
|
||
.filter((n: number) => Number.isFinite(n))
|
||
: undefined;
|
||
|
||
const documentType = content.documentType
|
||
? Number(content.documentType)
|
||
: undefined;
|
||
const correspondent = content.correspondent
|
||
? Number(content.correspondent)
|
||
: undefined;
|
||
const owner =
|
||
content.owner !== undefined &&
|
||
content.owner !== null &&
|
||
content.owner !== ''
|
||
? Number(content.owner)
|
||
: undefined;
|
||
|
||
const rawCustomFields: Record<string, string> | null =
|
||
content.customFields && typeof content.customFields === 'object'
|
||
? Object.fromEntries(
|
||
Object.entries(content.customFields as Record<string, any>).map(
|
||
([k, v]) => [k, applyTemplate(String(v ?? ''), variables)],
|
||
),
|
||
)
|
||
: null;
|
||
|
||
// Eingangsdatum: Priorität 1 = dediziertes Feld, Priorität 2 = Custom Field 9, Fallback = heute
|
||
const eingangsdatumTpl = String(content.eingangsdatum ?? '').trim();
|
||
let eingangsdatum: Date;
|
||
if (eingangsdatumTpl) {
|
||
eingangsdatum =
|
||
parseFlexDate(applyTemplate(eingangsdatumTpl, variables).trim()) ??
|
||
new Date();
|
||
} else if (rawCustomFields?.['9']) {
|
||
const parsed = parseFlexDate(rawCustomFields['9']);
|
||
if (parsed) {
|
||
eingangsdatum = parsed;
|
||
delete rawCustomFields['9']; // Task-Processor schreibt Field 9 in korrektem ISO-Format
|
||
} else {
|
||
eingangsdatum = new Date();
|
||
}
|
||
} else {
|
||
eingangsdatum = new Date();
|
||
}
|
||
|
||
// Custom Fields für den Upload zusammenstellen:
|
||
// - CF7 = InterneBelegnummer (für Duplikat-Check via Paperless API)
|
||
// - CF9 = Eingangsdatum in ISO-Format
|
||
// - User-konfigurierte Felder aus rawCustomFields
|
||
const uploadCustomFields: Record<string, string> = {};
|
||
if (rawCustomFields) {
|
||
for (const [k, v] of Object.entries(rawCustomFields))
|
||
uploadCustomFields[k] = v;
|
||
}
|
||
uploadCustomFields['7'] = interneBelegnummer;
|
||
uploadCustomFields['9'] =
|
||
`${eingangsdatum.getFullYear()}-${String(eingangsdatum.getMonth() + 1).padStart(2, '0')}-${String(eingangsdatum.getDate()).padStart(2, '0')}`;
|
||
|
||
// 6. Upload
|
||
const taskIdRaw = await this.paperlessService.uploadDocument(pdfPath, {
|
||
title,
|
||
filename: defaultFilenameBase || undefined,
|
||
documentType,
|
||
correspondent,
|
||
owner,
|
||
tags,
|
||
archiveSerialNumber,
|
||
customFields: uploadCustomFields,
|
||
});
|
||
const taskId = String(taskIdRaw).replace(/"/g, '');
|
||
|
||
// 7. Task anlegen
|
||
const task = this.taskRepo.create({
|
||
TaskId: taskId,
|
||
InterneBelegnummer: interneBelegnummer,
|
||
DocumentType: documentType ?? null,
|
||
Eingangsdatum: eingangsdatum,
|
||
Fertig: 0,
|
||
Tags: tags && tags.length > 0 ? tags.join(',') : null,
|
||
BetriebID: owner ?? null,
|
||
externeBelegnummer: null,
|
||
CustomFieldsJson:
|
||
rawCustomFields && Object.keys(rawCustomFields).length > 0
|
||
? JSON.stringify(rawCustomFields)
|
||
: null,
|
||
Asn: asn || null,
|
||
Lieferant: null,
|
||
EinkaufID: null,
|
||
Belegdatum: null,
|
||
TaskReferenceID: null,
|
||
BarcodeJson: null,
|
||
DuplikatZU: null,
|
||
});
|
||
await this.taskRepo.save(task);
|
||
|
||
this.logger.log(
|
||
`Dokument hochgeladen und Task angelegt: ${interneBelegnummer} → ${taskId}`,
|
||
);
|
||
return {};
|
||
}
|
||
}
|