Initial commit with Email Import Wizard and Task Processor updates
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
import { Controller, Get, Param, Post, Put, Delete, UseGuards, UseInterceptors, UploadedFile, Body, Logger, HttpException, HttpStatus, Res, Query } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PaperlessService } from './paperless.service';
|
||||
import { ApiKeyGuard } from '../auth/api-key.guard';
|
||||
import { UploadExternalDto } from './dto/upload-external.dto';
|
||||
import { Task } from '../database/entities/task.entity';
|
||||
import { Document } from '../database/entities/document.entity';
|
||||
import { DocumentField } from '../database/entities/document-field.entity';
|
||||
import { DocumentType } from '../database/entities/document-type.entity';
|
||||
|
||||
|
||||
@Controller('api/paperless')
|
||||
export class PaperlessController {
|
||||
private readonly logger = new Logger(PaperlessController.name);
|
||||
|
||||
constructor(
|
||||
private readonly paperlessService: PaperlessService,
|
||||
@InjectRepository(Task)
|
||||
private readonly taskRepo: Repository<Task>,
|
||||
@InjectRepository(Document)
|
||||
private readonly documentRepo: Repository<Document>,
|
||||
@InjectRepository(DocumentField)
|
||||
private readonly documentFieldRepo: Repository<DocumentField>,
|
||||
@InjectRepository(DocumentType)
|
||||
private readonly documentTypeRepo: Repository<DocumentType>,
|
||||
) {}
|
||||
|
||||
@Post('checksum')
|
||||
async checksumExists(@Body('checksum') checksum: string) {
|
||||
const exists = await this.paperlessService.checksumExists(checksum);
|
||||
return { exists };
|
||||
}
|
||||
|
||||
@Get('documents')
|
||||
async getDocuments(
|
||||
@Query('search') search?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
const params: any = {
|
||||
page: page || '1',
|
||||
page_size: pageSize || '20',
|
||||
};
|
||||
if (search) {
|
||||
params.query = search; // Global search in Paperless
|
||||
}
|
||||
return this.paperlessService.getDocuments(params);
|
||||
}
|
||||
|
||||
@Get('documents/:id')
|
||||
async getDocument(@Param('id') id: string) {
|
||||
return this.paperlessService.getDocument(parseInt(id, 10));
|
||||
}
|
||||
|
||||
@Get('tags')
|
||||
async getTags() {
|
||||
return this.paperlessService.getTags();
|
||||
}
|
||||
|
||||
@Get('tags/:id')
|
||||
async getTag(@Param('id') id: string) {
|
||||
// If the service doesn't have getTag(id), I should add it or just fetch all and find
|
||||
const tags = await this.paperlessService.getTags();
|
||||
return tags.find(t => t.id === parseInt(id, 10));
|
||||
}
|
||||
|
||||
@Get('document-types')
|
||||
async getDocumentTypes() {
|
||||
return this.paperlessService.getDocumentTypes();
|
||||
}
|
||||
|
||||
@Get('custom-fields')
|
||||
async getCustomFields() {
|
||||
return this.paperlessService.getCustomFields();
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
async getUsers() {
|
||||
return this.paperlessService.getUsers();
|
||||
}
|
||||
|
||||
@Get('correspondents')
|
||||
async getCorrespondents(@Query('search') search?: string) {
|
||||
const params: any = { page_size: 100 };
|
||||
if (search) {
|
||||
params.name__icontains = search;
|
||||
}
|
||||
const response = await this.paperlessService.getCorrespondents(params);
|
||||
return response.results;
|
||||
}
|
||||
|
||||
@Get('correspondents/:id')
|
||||
async getCorrespondent(@Param('id') id: string) {
|
||||
return this.paperlessService.getCorrespondent(parseInt(id, 10));
|
||||
}
|
||||
|
||||
@Get('inbox/list')
|
||||
@RequirePermissions(Permission.VIEW_INBOX)
|
||||
async getInboxList() {
|
||||
const documents = await this.paperlessService.getInboxDocuments();
|
||||
// In old C# logic: only return docs where archive_serial_number is not null
|
||||
return documents
|
||||
.filter((doc: any) => doc.archive_serial_number !== null && doc.archive_serial_number !== undefined)
|
||||
.map((doc: any) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
asn: doc.archive_serial_number,
|
||||
documentType: doc.document_type,
|
||||
correspondent: doc.correspondent,
|
||||
created: doc.created_date,
|
||||
added: doc.added,
|
||||
tags: doc.tags,
|
||||
customFields: doc.custom_fields,
|
||||
owner: doc.owner,
|
||||
}));
|
||||
}
|
||||
|
||||
@Get('manuell/list')
|
||||
@RequirePermissions(Permission.PROCESS_MANUALLY)
|
||||
async getManuellList() {
|
||||
const documents = await this.paperlessService.getManuellDocuments();
|
||||
return documents
|
||||
.filter((doc: any) => doc.archive_serial_number !== null && doc.archive_serial_number !== undefined)
|
||||
.map((doc: any) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
asn: doc.archive_serial_number,
|
||||
documentType: doc.document_type,
|
||||
correspondent: doc.correspondent,
|
||||
created: doc.created_date,
|
||||
added: doc.added,
|
||||
tags: doc.tags,
|
||||
customFields: doc.custom_fields,
|
||||
owner: doc.owner,
|
||||
}));
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('inbox/preview/:id')
|
||||
async getInboxPreview(@Param('id') id: string, @Res() res: Response) {
|
||||
try {
|
||||
const stream = await this.paperlessService.getDocumentPreviewStream(parseInt(id, 10));
|
||||
res.set({
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Disposition': `inline; filename="${id}preview.png"`,
|
||||
});
|
||||
stream.pipe(res);
|
||||
} catch (error) {
|
||||
if (!res.headersSent) {
|
||||
res.status(HttpStatus.INTERNAL_SERVER_ERROR).send('Error fetching preview');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('inbox/pdf/:id')
|
||||
async getInboxPdf(@Param('id') id: string, @Res() res: Response) {
|
||||
try {
|
||||
const stream = await this.paperlessService.getDocumentPdfStream(parseInt(id, 10));
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `inline; filename="${id}.pdf"`,
|
||||
});
|
||||
stream.pipe(res);
|
||||
} catch (error) {
|
||||
if (!res.headersSent) {
|
||||
res.status(HttpStatus.INTERNAL_SERVER_ERROR).send('Error fetching PDF');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Get('requirements/:id')
|
||||
async getRequirements(@Param('id') id: string, @Query('Posteingang') posteingang: string) {
|
||||
const documentTypeId = parseInt(id, 10);
|
||||
const isPosteingang = posteingang === '1';
|
||||
|
||||
const requirements = await this.documentFieldRepo.find({
|
||||
where: { DocumentType: documentTypeId },
|
||||
});
|
||||
|
||||
// Custom fields fetching inside here could be slow, but this is the simplest translation of the old API
|
||||
// Actually, getting all CFs doesn't take too long in Paperless API.
|
||||
const customFields = await this.paperlessService.getCustomFields();
|
||||
|
||||
const retVal: any[] = [];
|
||||
|
||||
for (const req of requirements) {
|
||||
if (isPosteingang && !req.VisiblePosteingang) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tmp: any = {
|
||||
id: req.Id,
|
||||
feldId: req.Type + (req.TypeIndex !== null ? '-' + req.TypeIndex : ''),
|
||||
hinweis: req.Hinweis || '',
|
||||
required: isPosteingang ? req.IsRequiredPosteingang : req.IsRequired,
|
||||
};
|
||||
|
||||
if (req.Type === 4) {
|
||||
// Custom Field
|
||||
const cfId = Number(req.TypeIndex);
|
||||
const cf = customFields.find((c: any) => c.id === cfId);
|
||||
tmp.isCustomField = true;
|
||||
tmp.customFieldIndex = cfId;
|
||||
if (cf) {
|
||||
tmp.feldName = cf.name;
|
||||
tmp.feldTyp = cf.id === 12 ? 'int' : cf.data_type;
|
||||
|
||||
if (cf.extra_data && cf.extra_data.select_options) {
|
||||
tmp.fieldOptions = cf.extra_data.select_options
|
||||
.filter((o: any) => o !== null)
|
||||
.map((o: any) => ({ id: o.id, label: o.label }));
|
||||
}
|
||||
} else {
|
||||
tmp.feldName = `Feld ${req.TypeIndex}`;
|
||||
tmp.feldTyp = 'string';
|
||||
}
|
||||
} else if (req.Type === 1) {
|
||||
tmp.feldName = 'Absender';
|
||||
tmp.feldTyp = 'select';
|
||||
const response = await this.paperlessService.getCorrespondents({ page_size: 9999 });
|
||||
const correspondents = response.results;
|
||||
tmp.fieldOptions = correspondents.map((c: any) => ({ id: c.id.toString(), label: c.name }));
|
||||
} else if (req.Type === 2) {
|
||||
tmp.feldName = 'Belegdatum';
|
||||
tmp.feldTyp = 'date';
|
||||
} else if (req.Type === 3) {
|
||||
tmp.feldName = 'Ablagenummer';
|
||||
tmp.feldTyp = 'string';
|
||||
} else if (req.Type === 5) {
|
||||
tmp.feldName = 'Titel';
|
||||
tmp.feldTyp = 'string';
|
||||
} else {
|
||||
tmp.feldName = tmp.feldId;
|
||||
tmp.feldTyp = 'string';
|
||||
}
|
||||
|
||||
retVal.push(tmp);
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Put('inbox/:id')
|
||||
async putInboxDocument(@Param('id') id: string, @Body() body: any) {
|
||||
const documentId = parseInt(id, 10);
|
||||
// Fetch from paperless
|
||||
const oldDocument = await this.paperlessService.getDocument(documentId);
|
||||
|
||||
oldDocument.document_type = body.documentType ?? oldDocument.document_type;
|
||||
oldDocument.owner = body.mandant ?? oldDocument.owner;
|
||||
oldDocument.correspondent = body.correspondent ?? oldDocument.correspondent;
|
||||
oldDocument.title = body.title ?? oldDocument.title;
|
||||
|
||||
if (body.date) {
|
||||
let docDate = new Date(body.date);
|
||||
if (docDate.getHours() > 22) {
|
||||
docDate = new Date(docDate.getTime() + 24 * 60 * 60 * 1000 - docDate.getHours() * 60 * 60 * 1000);
|
||||
}
|
||||
oldDocument.created_date = docDate.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
const cfDefinitions = await this.paperlessService.getCustomFields();
|
||||
|
||||
// update custom fields
|
||||
if (body.customFields) {
|
||||
for (const [key, value] of Object.entries(body.customFields)) {
|
||||
const fieldId = parseInt(key, 10);
|
||||
const cfDef = cfDefinitions.find((c: any) => c.id === fieldId);
|
||||
|
||||
let processedValue = value;
|
||||
if (cfDef?.data_type === 'documentlink' && value !== null && value !== '' && !Array.isArray(value)) {
|
||||
processedValue = [value];
|
||||
}
|
||||
|
||||
const existingFieldIndex = oldDocument.custom_fields.findIndex((f: any) => f.field === fieldId);
|
||||
|
||||
if (existingFieldIndex !== -1) {
|
||||
if (processedValue === null || processedValue === '') {
|
||||
oldDocument.custom_fields.splice(existingFieldIndex, 1);
|
||||
} else {
|
||||
oldDocument.custom_fields[existingFieldIndex].value = processedValue;
|
||||
}
|
||||
} else if (processedValue !== null && processedValue !== '') {
|
||||
oldDocument.custom_fields.push({ field: fieldId, value: processedValue });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Requirements check
|
||||
const reqs = await this.documentFieldRepo.find({
|
||||
where: { DocumentType: oldDocument.document_type },
|
||||
});
|
||||
|
||||
let isReady = true;
|
||||
let isReadyPosteingang = true;
|
||||
|
||||
for (const req of reqs) {
|
||||
let isFieldValid = false;
|
||||
if (req.Type === 1) isFieldValid = oldDocument.correspondent !== null;
|
||||
if (req.Type === 2) isFieldValid = oldDocument.created_date !== null;
|
||||
if (req.Type === 3) isFieldValid = oldDocument.archive_serial_number !== null;
|
||||
if (req.Type === 4) isFieldValid = !!oldDocument.custom_fields.find((cf: any) => cf.field === req.TypeIndex && cf.value !== null && cf.value !== '');
|
||||
if (req.Type === 5) isFieldValid = oldDocument.title !== null && oldDocument.title !== '';
|
||||
|
||||
if (req.IsRequired && !isFieldValid) isReady = false;
|
||||
if (req.IsRequiredPosteingang && !isFieldValid) isReadyPosteingang = false;
|
||||
}
|
||||
|
||||
const docType = await this.documentTypeRepo.findOne({
|
||||
where: { DocumentTypeId: oldDocument.document_type },
|
||||
});
|
||||
|
||||
oldDocument.tags = oldDocument.tags || [];
|
||||
|
||||
if (isReady) {
|
||||
oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1);
|
||||
if (docType?.TagNotReady) oldDocument.tags = oldDocument.tags.filter((t: number) => t !== docType.TagNotReady);
|
||||
if (docType?.TagReady && !oldDocument.tags.includes(docType.TagReady)) {
|
||||
oldDocument.tags.push(docType.TagReady);
|
||||
}
|
||||
|
||||
let titleTemplate = docType?.TitelTemplate || '';
|
||||
if (titleTemplate) {
|
||||
for (const cf of oldDocument.custom_fields) {
|
||||
const placeholder = `{{CUSTOM[${cf.field}]}}`;
|
||||
if (titleTemplate.includes(placeholder)) {
|
||||
titleTemplate = titleTemplate.replace(placeholder, cf.value?.toString() ?? '');
|
||||
}
|
||||
}
|
||||
titleTemplate = titleTemplate.replace('{{DATE}}', oldDocument.created_date);
|
||||
oldDocument.title = titleTemplate;
|
||||
}
|
||||
} else {
|
||||
if (docType?.TagNotReady) {
|
||||
if (isReadyPosteingang) {
|
||||
oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1);
|
||||
if (!oldDocument.tags.includes(docType.TagNotReady)) oldDocument.tags.push(docType.TagNotReady);
|
||||
} else {
|
||||
if (!oldDocument.tags.includes(1)) oldDocument.tags.push(1);
|
||||
}
|
||||
} else {
|
||||
if (!oldDocument.tags.includes(1)) oldDocument.tags.push(1);
|
||||
}
|
||||
if (docType?.TagReady) {
|
||||
oldDocument.tags = oldDocument.tags.filter((t: number) => t !== docType.TagReady);
|
||||
}
|
||||
}
|
||||
|
||||
await this.paperlessService.updateDocument(documentId, oldDocument);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Get('tasks')
|
||||
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||
async getAllTasks() {
|
||||
return this.taskRepo.find({ order: { Fertig: 'ASC' } });
|
||||
}
|
||||
|
||||
@Delete('tasks/fertig')
|
||||
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||
async deleteFertigeTasks() {
|
||||
const result = await this.taskRepo.delete({ Fertig: 1 });
|
||||
return { deleted: result.affected ?? 0 };
|
||||
}
|
||||
|
||||
@Delete('tasks/:taskId')
|
||||
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||
async deleteTask(@Param('taskId') taskId: string) {
|
||||
const result = await this.taskRepo.delete({ TaskId: taskId });
|
||||
if (!result.affected) {
|
||||
throw new HttpException('Task nicht gefunden', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return { deleted: result.affected };
|
||||
}
|
||||
|
||||
@Post('external-upload')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@UseInterceptors(FileInterceptor('document'))
|
||||
async externalUpload(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body() dto: UploadExternalDto,
|
||||
) {
|
||||
this.logger.log(`Externer Upload gestartet: ${dto.interneBelegnummer} (${file?.originalname})`);
|
||||
|
||||
try {
|
||||
// 0. Check if ASN already exists
|
||||
await this.paperlessService.validateAsnNotExists(dto.interneBelegnummer);
|
||||
|
||||
// 1. Forward to Paperless
|
||||
const paperlessTaskId = await this.paperlessService.uploadDocument(file.path, {
|
||||
title: `Beleg ${dto.interneBelegnummer}`,
|
||||
});
|
||||
|
||||
// 2. Create local Task
|
||||
const task = this.taskRepo.create({
|
||||
TaskId: paperlessTaskId.replace(/"/g, ''),
|
||||
InterneBelegnummer: dto.interneBelegnummer,
|
||||
DocumentType: dto.dokumentType,
|
||||
Eingangsdatum: dto.Eingangsdatum ? new Date(dto.Eingangsdatum) : null,
|
||||
Tags: dto.tag ? String(dto.tag) : null,
|
||||
BetriebID: dto.betriebId,
|
||||
Lieferant: dto.lieferant,
|
||||
EinkaufID: dto.einkaufId,
|
||||
externeBelegnummer: dto.externeBelegnummer,
|
||||
Belegdatum: dto.belegdatum ? new Date(dto.belegdatum) : null,
|
||||
TaskReferenceID: dto.parentId || '',
|
||||
BarcodeJson: dto.barcodeJson || '',
|
||||
DuplikatZU: dto.duplikatZu,
|
||||
Fertig: 0,
|
||||
});
|
||||
|
||||
await this.taskRepo.save(task);
|
||||
|
||||
this.logger.log(`Externer Upload erfolgreich: ${task.TaskId} für Beleg ${dto.interneBelegnummer}`);
|
||||
return task.TaskId;
|
||||
} catch (err) {
|
||||
this.logger.error(`Fehler beim externen Upload für Beleg ${dto.interneBelegnummer}: ${err.message}`, err.stack);
|
||||
throw new HttpException(
|
||||
`Fehler beim Verarbeiten des Dokumenten-Uploads: ${err.message}`,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user