Initial commit with Email Import Wizard and Task Processor updates

This commit is contained in:
2026-05-04 08:02:11 +02:00
commit effdc5d59f
170 changed files with 67739 additions and 0 deletions
@@ -0,0 +1,204 @@
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DocumentField } from '../database/entities/document-field.entity';
import { DocumentType } from '../database/entities/document-type.entity';
import { PaperlessService } from './paperless.service';
import { PostprocessingService } from '../postprocessing/postprocessing.service';
@Injectable()
export class PaperlessProcessorService {
private readonly logger = new Logger(PaperlessProcessorService.name);
constructor(
private readonly configService: ConfigService,
private readonly paperlessService: PaperlessService,
private readonly postprocessingService: PostprocessingService,
@InjectRepository(DocumentType) private readonly docTypeRepo: Repository<DocumentType>,
@InjectRepository(DocumentField) private readonly docFieldRepo: Repository<DocumentField>,
) {}
@Cron(process.env.PAPERLESS_PROCESSOR_CRON || '0 * * * * *')
async processDocuments() {
try {
const response = await this.paperlessService.getDocuments({ tags__id__all: 16, page_size: 9999 });
const documents: any[] = Array.isArray(response) ? response : (response?.results ?? []);
if (documents.length === 0) return;
const customFields = await this.paperlessService.getCustomFields();
const validFieldIds = new Set(customFields.map((f: any) => f.id));
this.logger.log(`Verarbeite ${documents.length} Dokument(e) mit Tag "paperlessmanager" (ID: 16).`);
for (const doc of documents) {
try {
const updatedDoc = await this.processSingleDocument(doc, validFieldIds);
// Postprocessing nach dem Speichern evaluieren
await this.postprocessingService.evaluate(updatedDoc || doc);
} catch (innerErr: any) {
this.logger.error(`Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`);
if (innerErr.response?.data) {
this.logger.error(`Paperless API Response: ${JSON.stringify(innerErr.response.data)}`);
}
}
}
} catch (err) {
this.logger.error('Fehler bei der Dokumentenverarbeitung:', err);
}
}
private async processSingleDocument(doc: any, validFieldIds: Set<number>): Promise<any> {
this.logger.log(`Verarbeite Dokument ID: ${doc.id}`);
if (!doc.document_type) {
this.logger.warn(`Dokument ${doc.id} hat keinen Dokumenten-Typen setze Tag 17.`);
const tagsSet = new Set<number>(doc.tags || []);
tagsSet.add(17);
if (!tagsSet.has(1)) {
tagsSet.add(6);
}
const updated = await this.paperlessService.updateDocument(doc.id, { tags: Array.from(tagsSet) });
return updated;
}
const docTypeConfig = await this.docTypeRepo.findOne({
where: { DocumentTypeId: doc.document_type },
});
if (!docTypeConfig) {
this.logger.warn(`Konfiguration für DocumentType ${doc.document_type} nicht in der Datenbank gefunden.`);
return null;
}
const fieldsConfig = await this.docFieldRepo.find({
where: { DocumentType: doc.document_type },
});
if (fieldsConfig.length === 0) {
this.logger.log(`Dokument ${doc.id} (Typ ${doc.document_type}) hat keine Dokument-Felder in der DB konfiguriert.`);
const newTagsNoFields = Array.from(new Set([...(doc.tags || []), 17]));
const updated = await this.paperlessService.updateDocument(doc.id, { tags: newTagsNoFields });
return updated;
}
const currentCustomFields = doc.custom_fields || [];
const newCustomFields = [...currentCustomFields];
let isAllRequiredFilled = true;
for (const fieldConf of fieldsConfig) {
if (fieldConf.Type === 4) {
const customFieldId = fieldConf.TypeIndex;
if (!customFieldId) continue;
if (!validFieldIds.has(customFieldId)) {
this.logger.warn(`Überspringe ungültiges Custom Field (TypeIndex: ${customFieldId}) für Dokument ${doc.id} - in Paperless nicht vorhanden.`);
continue;
}
const existingField = newCustomFields.find(f => f.field === customFieldId);
let isFilled = false;
if (existingField) {
isFilled = existingField.value !== null && existingField.value !== '';
} else {
newCustomFields.push({ field: customFieldId, value: null });
}
if (fieldConf.IsRequired && !isFilled) {
isAllRequiredFilled = false;
}
} else {
if (fieldConf.IsRequired) {
let isFilled = false;
switch (fieldConf.Type) {
case 1:
isFilled = doc.correspondent !== null && doc.correspondent !== undefined;
break;
case 2:
isFilled = !!doc.created || !!doc.created_date;
break;
case 3:
isFilled = doc.archive_serial_number !== null && doc.archive_serial_number !== undefined;
break;
case 5:
isFilled = !!doc.title;
break;
default:
isFilled = true;
}
if (!isFilled) {
isAllRequiredFilled = false;
}
}
}
}
const tagsSet = new Set<number>(doc.tags || []);
if (isAllRequiredFilled) {
if (docTypeConfig.TagReady) tagsSet.add(docTypeConfig.TagReady);
if (docTypeConfig.TagNotReady) tagsSet.delete(docTypeConfig.TagNotReady);
} else {
if (docTypeConfig.TagNotReady) tagsSet.add(docTypeConfig.TagNotReady);
if (docTypeConfig.TagReady) tagsSet.delete(docTypeConfig.TagReady);
}
tagsSet.add(17);
// Titel-Template auflösen
const updatePayload: any = {
custom_fields: newCustomFields,
tags: Array.from(tagsSet),
};
if (docTypeConfig.TitelTemplate) {
let title = docTypeConfig.TitelTemplate;
// Prüfen ob alle im Template referenzierten Custom Fields ausgefüllt sind
const placeholderRegex = /\{\{CUSTOM\[(\d+)\]\}\}/g;
let allFilled = true;
let match: RegExpExecArray | null;
while ((match = placeholderRegex.exec(title)) !== null) {
const fieldId = parseInt(match[1], 10);
const cf = newCustomFields.find(f => f.field === fieldId);
if (!cf || cf.value == null || cf.value === '') {
allFilled = false;
break;
}
}
if (allFilled) {
for (const cf of newCustomFields) {
const placeholder = `{{CUSTOM[${cf.field}]}}`;
if (title.includes(placeholder)) {
title = title.replaceAll(placeholder, cf.value != null ? String(cf.value) : '');
}
}
if (title.includes('{{DATE}}')) {
const created = doc.created ? new Date(doc.created) : new Date();
const dateStr = `${String(created.getDate()).padStart(2, '0')}.${String(created.getMonth() + 1).padStart(2, '0')}.${created.getFullYear()}`;
title = title.replaceAll('{{DATE}}', dateStr);
}
if (title.includes('{{ZEITSTEMPEL}}')) {
const now = new Date();
const ts = `${String(now.getDate()).padStart(2, '0')}.${String(now.getMonth() + 1).padStart(2, '0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
title = title.replaceAll('{{ZEITSTEMPEL}}', ts);
}
updatePayload.title = title;
} else {
this.logger.log(`Dokument ${doc.id}: Titel-Template nicht angewendet nicht alle referenzierten Custom Fields ausgefüllt.`);
}
}
const updated = await this.paperlessService.updateDocument(doc.id, updatePayload);
this.logger.log(`Dokument ${doc.id} erfolgreich aktualisiert (Alle Pflichtfelder vorhanden: ${isAllRequiredFilled}).`);
return updated;
}
}