feat: Duplikate im Import als neue Version behandeln #6

Merged
bjoernpoettker merged 12 commits from feat/duplikat-neue-version into main 2026-11-02 13:31:04 +00:00
8 changed files with 89 additions and 24 deletions
Showing only changes of commit c1afa02ee2 - Show all commits

No files matched your search

@@ -166,8 +166,11 @@ Geprüft wird immer gegen Paperless, und zwar über **beide** Merkmale, die aus
der internen Belegnummer folgen: der internen Belegnummer folgen:
- CF7 = interne Belegnummer (z. B. `2026-000123`) - CF7 = interne Belegnummer (z. B. `2026-000123`)
- ASN = interne Belegnummer ohne Bindestrich (z. B. `2026000123`) – auch im - ASN = die ASN, die das Dokument beim Upload **tatsächlich bekäme**: im
Posteingang, unabhängig von einem im Template separat hinterlegten ASN-Feld. Posteingang aus dem ASN-Feld der Aktion, falls gesetzt, sonst aus der
Belegnummer ohne Bindestrich (z. B. `2026000123`); im E-Mail-Import immer aus
der Belegnummer. Prüfung, Versions-Upload und normaler Upload verwenden
dieselbe ASN.
„Als neue Version hinzufügen“ wird nur angeboten, wenn alle Treffer auf **genau „Als neue Version hinzufügen“ wird nur angeboten, wenn alle Treffer auf **genau
ein** Dokument zeigen. Zeigen CF7- und ASN-Treffer auf verschiedene Dokumente ein** Dokument zeigen. Zeigen CF7- und ASN-Treffer auf verschiedene Dokumente
@@ -1,6 +1,9 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { InboxPostprocessorService } from './inbox-postprocessor.service'; import { InboxPostprocessorService } from './inbox-postprocessor.service';
import { originalSeitenDesAbschnitts } from './paperless-entscheidung.util'; import {
ermittleUploadAsn,
originalSeitenDesAbschnitts,
} from './paperless-entscheidung.util';
describe('InboxPostprocessorService.runForDocument', () => { describe('InboxPostprocessorService.runForDocument', () => {
it('nimmt eine Duplikat-Entscheidung nur für genau einen Abschnitt an', async () => { it('nimmt eine Duplikat-Entscheidung nur für genau einen Abschnitt an', async () => {
@@ -25,3 +28,18 @@ describe('originalSeitenDesAbschnitts', () => {
expect(originalSeitenDesAbschnitts([1, 3, 4, 5], [1, 2])).toEqual([3, 4]); expect(originalSeitenDesAbschnitts([1, 3, 4, 5], [1, 2])).toEqual([3, 4]);
}); });
}); });
describe('ermittleUploadAsn', () => {
it('nimmt die ASN aus dem Template-Feld, wenn gesetzt', () => {
expect(ermittleUploadAsn('ASN 555', '2026-000123')).toBe(555);
});
it('bildet sie sonst aus der Belegnummer', () => {
expect(ermittleUploadAsn('', '2026-000123')).toBe(2026000123);
expect(ermittleUploadAsn(null, '2026-000123')).toBe(2026000123);
});
it('liefert undefined, wenn keine Ziffern vorhanden sind', () => {
expect(ermittleUploadAsn(null, 'abc')).toBeUndefined();
});
});
@@ -26,6 +26,7 @@ import { applyTemplate, buildVariables } from './variable-resolver';
import { DokumentVersionService } from '../paperless/dokument-version.service'; import { DokumentVersionService } from '../paperless/dokument-version.service';
import { import {
aktionenAbEntscheidung, aktionenAbEntscheidung,
ermittleUploadAsn,
originalSeitenDesAbschnitts, originalSeitenDesAbschnitts,
type PaperlessEntscheidung, type PaperlessEntscheidung,
} from './paperless-entscheidung.util'; } from './paperless-entscheidung.util';
@@ -471,12 +472,18 @@ export class InboxPostprocessorService {
if (!interneBelegnummer) if (!interneBelegnummer)
throw new Error('Interne Belegnummer konnte nicht aufgelöst werden'); throw new Error('Interne Belegnummer konnte nicht aufgelöst werden');
// 2. Vom Benutzer bestätigt: als neue Version an das Stammdokument hängen // 2. ASN, die das Dokument beim Upload bekäme – gegen genau diese wird geprüft
const asnTpl = String(content.asn ?? '').trim();
const asn = asnTpl ? applyTemplate(asnTpl, variables).trim() : null;
const archiveSerialNumber = ermittleUploadAsn(asn, interneBelegnummer);
// 3. Vom Benutzer bestätigt: als neue Version an das Stammdokument hängen
if (entscheidung?.aktion === 'version') { if (entscheidung?.aktion === 'version') {
await this.dokumentVersion.haengeVersionAn( await this.dokumentVersion.haengeVersionAn(
entscheidung.documentId, entscheidung.documentId,
pdfPath, pdfPath,
interneBelegnummer, interneBelegnummer,
archiveSerialNumber ?? null,
); );
this.logger.log( this.logger.log(
`Belegnummer ${interneBelegnummer} als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`, `Belegnummer ${interneBelegnummer} als neue Version zu Dokument ${entscheidung.documentId} hinzugefügt`,
@@ -486,22 +493,11 @@ export class InboxPostprocessorService {
}; };
} }
// 3. ASN bestimmen (für den Upload) // 4. Duplikat-Check Paperless: CF7 + Upload-ASN
const asnTpl = String(content.asn ?? '').trim(); const pruefung = await this.paperlessService.pruefeBelegnummer(
const asn = asnTpl ? applyTemplate(asnTpl, variables).trim() : null; interneBelegnummer,
let archiveSerialNumber: number | undefined; archiveSerialNumber ?? null,
if (asn) { );
const n = parseInt(asn.replace(/[^0-9]/g, ''), 10);
if (!Number.isNaN(n)) archiveSerialNumber = n;
}
if (archiveSerialNumber === undefined) {
const n = parseInt(interneBelegnummer.replace(/[^0-9]/g, ''), 10);
if (!Number.isNaN(n)) archiveSerialNumber = n;
}
// 4. Duplikat-Check Paperless: CF7 + aus der Belegnummer abgeleitete ASN
const pruefung =
await this.paperlessService.pruefeBelegnummer(interneBelegnummer);
if (pruefung.status !== 'frei') { if (pruefung.status !== 'frei') {
this.logger.warn( this.logger.warn(
`Duplikat (Paperless): Belegnummer ${interneBelegnummer} bereits vorhanden (Dokument(e) ${pruefung.kandidaten.join(', ')})`, `Duplikat (Paperless): Belegnummer ${interneBelegnummer} bereits vorhanden (Dokument(e) ${pruefung.kandidaten.join(', ')})`,
@@ -27,3 +27,19 @@ export function originalSeitenDesAbschnitts(
): number[] { ): number[] {
return pageIndices.map((idx) => survivingOriginalPages[idx]); return pageIndices.map((idx) => survivingOriginalPages[idx]);
} }
/**
* ASN, die das Dokument beim Upload bekommt: aus dem ASN-Feld der Aktion
* (bereits aufgelöst), sonst aus der internen Belegnummer – jeweils nur die Ziffern.
*/
export function ermittleUploadAsn(
asnAusTemplate: string | null,
interneBelegnummer: string,
): number | undefined {
for (const quelle of [asnAusTemplate, interneBelegnummer]) {
if (!quelle) continue;
const n = parseInt(quelle.replace(/[^0-9]/g, ''), 10);
if (!Number.isNaN(n)) return n;
}
return undefined;
}
@@ -56,6 +56,17 @@ describe('DokumentVersionService.haengeVersionAn', () => {
expect(paperless.uploadDocumentVersion).not.toHaveBeenCalled(); expect(paperless.uploadDocumentVersion).not.toHaveBeenCalled();
}); });
it('prüft die Upload-ASN, wenn sie übergeben wird', async () => {
paperless.getDocument.mockResolvedValue({ id: 42, tags: [] });
await service.haengeVersionAn(42, '/tmp/x.pdf', '2026-000123', 555);
expect(paperless.pruefeBelegnummer).toHaveBeenCalledWith(
'2026-000123',
555,
);
});
it('wartet auf die Paperless-Verarbeitung der Version', async () => { it('wartet auf die Paperless-Verarbeitung der Version', async () => {
paperless.getDocument.mockResolvedValue({ id: 42, tags: [] }); paperless.getDocument.mockResolvedValue({ id: 42, tags: [] });
paperless.getTask paperless.getTask
@@ -32,8 +32,12 @@ export class DokumentVersionService {
rootDocumentId: number, rootDocumentId: number,
pdfPath: string, pdfPath: string,
belegnummer: string, belegnummer: string,
uploadAsn?: number | null,
): Promise<void> { ): Promise<void> {
const pruefung = await this.paperlessService.pruefeBelegnummer(belegnummer); const pruefung = await this.paperlessService.pruefeBelegnummer(
belegnummer,
uploadAsn,
);
if ( if (
pruefung.status !== 'eindeutig' || pruefung.status !== 'eindeutig' ||
pruefung.documentId !== rootDocumentId pruefung.documentId !== rootDocumentId
@@ -85,4 +85,17 @@ describe('PaperlessService – Belegnummer-Suche', () => {
kandidaten: [42], kandidaten: [42],
}); });
}); });
it('prüft die übergebene Upload-ASN statt der aus der Belegnummer gebildeten', async () => {
get.mockResolvedValue({ data: { results: [] } });
await service.pruefeBelegnummer('2026-000123', 555);
const asnWerte = (
get.mock.calls as [string, { params: Record<string, unknown> }][]
)
.map(([, cfg]) => cfg.params.archive_serial_number)
.filter((wert) => wert !== undefined);
expect(asnWerte).toEqual([555]);
});
}); });
@@ -500,12 +500,16 @@ export class PaperlessService {
/** /**
* Prüft, ob die interne Belegnummer in Paperless schon vergeben ist – über * Prüft, ob die interne Belegnummer in Paperless schon vergeben ist – über
* CF7 und die daraus abgeleitete ASN. * CF7 und die ASN, die das Dokument beim Upload bekäme (`uploadAsn`; ohne
* Angabe die aus der Belegnummer gebildete).
*/ */
async pruefeBelegnummer(belegnummer: string): Promise<BelegnummerPruefung> { async pruefeBelegnummer(
belegnummer: string,
uploadAsn?: number | null,
): Promise<BelegnummerPruefung> {
const nummer = (belegnummer ?? '').trim(); const nummer = (belegnummer ?? '').trim();
if (!nummer) return klassifiziereTreffer([], []); if (!nummer) return klassifiziereTreffer([], []);
const asn = belegnummerZuAsn(nummer); const asn = uploadAsn ?? belegnummerZuAsn(nummer);
const [cf7, asnTreffer] = await Promise.all([ const [cf7, asnTreffer] = await Promise.all([
this.findRootDocumentIdsByCustomField(7, nummer), this.findRootDocumentIdsByCustomField(7, nummer),
asn !== null asn !== null