feat(paperless): Belegnummer gegen Paperless prüfen (CF7 + ASN)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da02baa2b9
commit
57a460ffa2
5 files changed
+187
No files matched your search
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
belegnummerZuAsn,
|
||||
klassifiziereTreffer,
|
||||
} from './belegnummer-pruefung.util';
|
||||
|
||||
describe('belegnummerZuAsn', () => {
|
||||
it('entfernt Bindestriche und liefert die Zahl', () => {
|
||||
expect(belegnummerZuAsn('2026-000123')).toBe(2026000123);
|
||||
});
|
||||
|
||||
it('liefert null für leere oder nicht numerische Belegnummern', () => {
|
||||
expect(belegnummerZuAsn('')).toBeNull();
|
||||
expect(belegnummerZuAsn('abc')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('klassifiziereTreffer', () => {
|
||||
it('ohne Treffer ist die Belegnummer frei', () => {
|
||||
expect(klassifiziereTreffer([], [])).toEqual({
|
||||
status: 'frei',
|
||||
documentId: null,
|
||||
kandidaten: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('CF7 und ASN auf demselben Dokument sind eindeutig', () => {
|
||||
expect(klassifiziereTreffer([42], [42])).toEqual({
|
||||
status: 'eindeutig',
|
||||
documentId: 42,
|
||||
kandidaten: [42],
|
||||
});
|
||||
});
|
||||
|
||||
it('Stamm und Version (gleiche Stamm-ID) zählen als ein Dokument', () => {
|
||||
expect(klassifiziereTreffer([42, 42], [])).toEqual({
|
||||
status: 'eindeutig',
|
||||
documentId: 42,
|
||||
kandidaten: [42],
|
||||
});
|
||||
});
|
||||
|
||||
it('verschiedene Dokumente sind mehrdeutig', () => {
|
||||
expect(klassifiziereTreffer([42], [7])).toEqual({
|
||||
status: 'mehrdeutig',
|
||||
documentId: null,
|
||||
kandidaten: [7, 42],
|
||||
});
|
||||
});
|
||||
|
||||
it('ein Treffer nur über die ASN ist eindeutig', () => {
|
||||
expect(klassifiziereTreffer([], [7])).toEqual({
|
||||
status: 'eindeutig',
|
||||
documentId: 7,
|
||||
kandidaten: [7],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
export type BelegnummerStatus = 'frei' | 'eindeutig' | 'mehrdeutig';
|
||||
|
||||
export interface BelegnummerPruefung {
|
||||
status: BelegnummerStatus;
|
||||
/** Stammdokument, an das eine neue Version gehängt werden kann (nur bei „eindeutig“) */
|
||||
documentId: number | null;
|
||||
/** Alle gefundenen Stammdokumente, aufsteigend */
|
||||
kandidaten: number[];
|
||||
}
|
||||
|
||||
/** ASN aus der internen Belegnummer: `2026-000123` → `2026000123`. */
|
||||
export function belegnummerZuAsn(belegnummer: string): number | null {
|
||||
const ziffern = (belegnummer ?? '').replace(/-/g, '').trim();
|
||||
if (!ziffern) return null;
|
||||
const asn = parseInt(ziffern, 10);
|
||||
return Number.isNaN(asn) ? null : asn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die Treffer der CF7- und ASN-Suche (bereits auf Stammdokumente
|
||||
* normalisiert) zusammen. Nur genau ein Stammdokument ist „eindeutig“.
|
||||
*/
|
||||
export function klassifiziereTreffer(
|
||||
cf7RootIds: number[],
|
||||
asnRootIds: number[],
|
||||
): BelegnummerPruefung {
|
||||
const kandidaten = [...new Set([...cf7RootIds, ...asnRootIds])].sort(
|
||||
(a, b) => a - b,
|
||||
);
|
||||
if (kandidaten.length === 0)
|
||||
return { status: 'frei', documentId: null, kandidaten };
|
||||
if (kandidaten.length === 1)
|
||||
return { status: 'eindeutig', documentId: kandidaten[0], kandidaten };
|
||||
return { status: 'mehrdeutig', documentId: null, kandidaten };
|
||||
}
|
||||
@@ -81,6 +81,13 @@ export class PaperlessController {
|
||||
return { exists };
|
||||
}
|
||||
|
||||
/** Ist die interne Belegnummer in Paperless schon vergeben (CF7 + ASN)? */
|
||||
@Get('belegnummer-pruefung')
|
||||
@RequirePermissions(Permission.VIEW_MAIL)
|
||||
async pruefeBelegnummer(@Query('belegnummer') belegnummer: string) {
|
||||
return this.paperlessService.pruefeBelegnummer(belegnummer ?? '');
|
||||
}
|
||||
|
||||
@Get('documents')
|
||||
async getDocuments(
|
||||
@Query('search') search?: string,
|
||||
|
||||
@@ -4,6 +4,19 @@ import axios, { type AxiosInstance } from 'axios';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import FormData = require('form-data');
|
||||
import {
|
||||
type BelegnummerPruefung,
|
||||
belegnummerZuAsn,
|
||||
klassifiziereTreffer,
|
||||
} from './belegnummer-pruefung.util';
|
||||
|
||||
/** Ausschnitt eines Paperless-Dokuments, wie ihn die Belegnummer-Suche braucht. */
|
||||
interface PaperlessTrefferDokument {
|
||||
id: number;
|
||||
root_document?: number | null;
|
||||
archive_serial_number?: number | null;
|
||||
custom_fields?: { field: number; value: string | number | boolean | null }[];
|
||||
}
|
||||
|
||||
export interface PaperlessTaskStatus {
|
||||
taskId: string;
|
||||
@@ -396,4 +409,68 @@ export class PaperlessService {
|
||||
);
|
||||
return match ? Number(match.id) : null;
|
||||
}
|
||||
|
||||
/** Stamm-ID eines Treffers – Versionen zeigen über `root_document` auf ihren Stamm. */
|
||||
private static rootId(doc: PaperlessTrefferDokument): number {
|
||||
return Number(doc.root_document ?? doc.id);
|
||||
}
|
||||
|
||||
private async sucheDokumente(
|
||||
params: Record<string, unknown>,
|
||||
): Promise<PaperlessTrefferDokument[]> {
|
||||
const response = await this.client.get<{
|
||||
results?: PaperlessTrefferDokument[];
|
||||
}>('/documents/', {
|
||||
params: { ...params, page_size: 100, truncate_content: true },
|
||||
});
|
||||
return response.data.results ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Stammdokumente, deren Custom Field exakt (case-insensitiv) den Wert hat.
|
||||
*/
|
||||
async findRootDocumentIdsByCustomField(
|
||||
fieldId: number,
|
||||
value: string,
|
||||
): Promise<number[]> {
|
||||
const treffer = await this.sucheDokumente({
|
||||
custom_field_query: JSON.stringify([fieldId, 'icontains', value]),
|
||||
});
|
||||
const valueLower = value.toLowerCase();
|
||||
return treffer
|
||||
.filter((doc) =>
|
||||
(doc.custom_fields ?? []).some(
|
||||
(cf) =>
|
||||
cf.field === fieldId &&
|
||||
String(cf.value ?? '').toLowerCase() === valueLower,
|
||||
),
|
||||
)
|
||||
.map((doc) => PaperlessService.rootId(doc));
|
||||
}
|
||||
|
||||
/** Alle Stammdokumente mit genau dieser ASN. */
|
||||
async findRootDocumentIdsByAsn(asn: number): Promise<number[]> {
|
||||
if (!Number.isFinite(asn)) return [];
|
||||
const treffer = await this.sucheDokumente({ archive_serial_number: asn });
|
||||
return treffer
|
||||
.filter((doc) => Number(doc.archive_serial_number) === asn)
|
||||
.map((doc) => PaperlessService.rootId(doc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob die interne Belegnummer in Paperless schon vergeben ist – über
|
||||
* CF7 und die daraus abgeleitete ASN.
|
||||
*/
|
||||
async pruefeBelegnummer(belegnummer: string): Promise<BelegnummerPruefung> {
|
||||
const nummer = (belegnummer ?? '').trim();
|
||||
if (!nummer) return klassifiziereTreffer([], []);
|
||||
const asn = belegnummerZuAsn(nummer);
|
||||
const [cf7, asnTreffer] = await Promise.all([
|
||||
this.findRootDocumentIdsByCustomField(7, nummer),
|
||||
asn !== null
|
||||
? this.findRootDocumentIdsByAsn(asn)
|
||||
: Promise.resolve([] as number[]),
|
||||
]);
|
||||
return klassifiziereTreffer(cf7, asnTreffer);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user