Files
paperlessmanager/paperless-backend/src/zahlung/zahlung.service.ts
T
bjoernpoettker 4d05a94681 feat: implement two-step Freigabe→Zahlung workflow
Adds a payment step after document approval: PM_Freigabe approves
(Field 15 = "freigegeben"), then PM_Zahlung can mark as paid (Field 16).

- Backend: VIEW_ZAHLUNG permission mapped to PM_Zahlung OIDC group
- Backend: ZahlungModule with endpoints to list documents by filter
  (ausstehend/freigegeben/alle), set Field 16, fetch options from Paperless
- Backend: setZahlung() throws ForbiddenException if Field 15 ≠ "freigegeben"
- Frontend: /zahlung route with 3-way filter, two status columns (Freigabe + Zahlung)
- Frontend: "Zahlung verbuchen" button disabled with tooltip for non-approved docs
- Frontend: Zahlung menu item with EuroOutlined icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 21:29:50 +02:00

125 lines
4.2 KiB
TypeScript

import { Injectable, Logger, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DocumentType } from '../database/entities/document-type.entity';
import { PaperlessService } from '../paperless/paperless.service';
const FREIGABE_FIELD_ID = 15;
const ZAHLUNG_FIELD_ID = 16;
const FREIGABE_WERT_FREIGEGEBEN = 'freigegeben';
export type ZahlungFilter = 'ausstehend' | 'freigegeben' | 'alle';
@Injectable()
export class ZahlungService {
private readonly logger = new Logger(ZahlungService.name);
constructor(
@InjectRepository(DocumentType)
private readonly documentTypeRepo: Repository<DocumentType>,
private readonly paperlessService: PaperlessService,
) {}
async getZahlungDocuments(page: number, pageSize: number, filter: ZahlungFilter) {
const docTypes = await this.documentTypeRepo.find({
where: { FreigabeErforderlich: true as any },
});
if (docTypes.length === 0) {
return { count: 0, results: [] };
}
const docTypeIds = docTypes.map((dt) => dt.DocumentTypeId).join(',');
const params: Record<string, any> = {
page: 1,
page_size: 9999,
document_type__id__in: docTypeIds,
ordering: '-created',
truncate_content: true,
};
let allDocs: any[];
try {
const result = await this.paperlessService.getDocuments(params);
allDocs = result.results ?? [];
} catch (err: any) {
this.logger.warn('Fehler beim Laden der Belege für Zahlung', err?.message);
return { count: 0, results: [] };
}
const filtered = this.applyFilter(allDocs, filter);
const start = (page - 1) * pageSize;
return {
count: filtered.length,
results: filtered.slice(start, start + pageSize),
};
}
private applyFilter(docs: any[], filter: ZahlungFilter): any[] {
if (filter === 'alle') return docs;
return docs.filter((doc: any) => {
const freigabeValue = this.getCfValue(doc, FREIGABE_FIELD_ID);
const istFreigegeben = freigabeValue === FREIGABE_WERT_FREIGEGEBEN;
if (filter === 'freigegeben') return istFreigegeben;
// 'ausstehend': freigegeben aber noch nicht bezahlt
const zahlungValue = this.getCfValue(doc, ZAHLUNG_FIELD_ID);
return istFreigegeben && !zahlungValue;
});
}
private getCfValue(doc: any, fieldId: number): string | null {
const cf = (doc.custom_fields ?? []).find((f: any) => f.field === fieldId);
if (!cf || cf.value === null || cf.value === undefined || cf.value === '') return null;
if (typeof cf.value === 'object') {
return String(cf.value?.id ?? cf.value?.value ?? cf.value?.label ?? '') || null;
}
return String(cf.value);
}
async setZahlung(documentId: number, value: string | null) {
const doc = await this.paperlessService.getDocument(documentId);
const freigabeValue = this.getCfValue(doc, FREIGABE_FIELD_ID);
if (freigabeValue !== FREIGABE_WERT_FREIGEGEBEN) {
throw new ForbiddenException(
'Zahlung kann nur für freigegebene Belege gesetzt werden',
);
}
const customFields: any[] = [...(doc.custom_fields ?? [])];
const existing = customFields.find((f: any) => f.field === ZAHLUNG_FIELD_ID);
if (existing) {
existing.value = value;
} else if (value !== null && value !== '') {
customFields.push({ field: ZAHLUNG_FIELD_ID, value });
}
await this.paperlessService.updateDocument(documentId, { custom_fields: customFields });
return { success: true };
}
async getZahlungOptions(): Promise<{ id: string; label: string }[]> {
const fields = await this.paperlessService.getCustomFields();
const field = (fields as any[]).find((f: any) => f.id === ZAHLUNG_FIELD_ID);
if (!field) return [];
const rawOptions: any[] = field.extra_data?.select_options ?? [];
return rawOptions
.filter((o) => o !== null && o !== undefined && o !== '')
.map((o) => {
if (typeof o === 'object') {
return {
id: String(o.id ?? o.value ?? o.label ?? ''),
label: String(o.label ?? o.name ?? o.id ?? ''),
};
}
return { id: String(o), label: String(o) };
})
.filter((o) => o.id !== '');
}
}