feat: Duplikate im Import als neue Version behandeln #6
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 };
|
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')
|
@Get('documents')
|
||||||
async getDocuments(
|
async getDocuments(
|
||||||
@Query('search') search?: string,
|
@Query('search') search?: string,
|
||||||
|
|||||||
@@ -4,6 +4,19 @@ import axios, { type AxiosInstance } from 'axios';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import FormData = require('form-data');
|
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 {
|
export interface PaperlessTaskStatus {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
@@ -396,4 +409,68 @@ export class PaperlessService {
|
|||||||
);
|
);
|
||||||
return match ? Number(match.id) : null;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
import api from './client';
|
import api from './client';
|
||||||
|
|
||||||
|
export type BelegnummerStatus = 'frei' | 'eindeutig' | 'mehrdeutig';
|
||||||
|
|
||||||
|
export interface BelegnummerPruefung {
|
||||||
|
status: BelegnummerStatus;
|
||||||
|
/** Stammdokument für eine neue Version (nur bei „eindeutig“) */
|
||||||
|
documentId: number | null;
|
||||||
|
kandidaten: number[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaperlessTag {
|
export interface PaperlessTag {
|
||||||
id: number;
|
id: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -62,4 +71,6 @@ export const paperlessApi = {
|
|||||||
api.post<{ exists: boolean }>('/api/paperless/checksum', { checksum }).then(r => r.data.exists),
|
api.post<{ exists: boolean }>('/api/paperless/checksum', { checksum }).then(r => r.data.exists),
|
||||||
getDocumentPdfBlob: (id: number) =>
|
getDocumentPdfBlob: (id: number) =>
|
||||||
api.get<Blob>(`/api/paperless/inbox/pdf/${id}`, { responseType: 'blob' }).then(r => r.data),
|
api.get<Blob>(`/api/paperless/inbox/pdf/${id}`, { responseType: 'blob' }).then(r => r.data),
|
||||||
|
pruefeBelegnummer: (belegnummer: string) =>
|
||||||
|
api.get<BelegnummerPruefung>('/api/paperless/belegnummer-pruefung', { params: { belegnummer } }).then(r => r.data),
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user