diff --git a/docs/superpowers/specs/2026-11-02-duplikat-neue-version-design.md b/docs/superpowers/specs/2026-11-02-duplikat-neue-version-design.md index bc3feb6..d21d982 100644 --- a/docs/superpowers/specs/2026-11-02-duplikat-neue-version-design.md +++ b/docs/superpowers/specs/2026-11-02-duplikat-neue-version-design.md @@ -166,8 +166,11 @@ Geprüft wird immer gegen Paperless, und zwar über **beide** Merkmale, die aus der internen Belegnummer folgen: - CF7 = interne Belegnummer (z. B. `2026-000123`) -- ASN = interne Belegnummer ohne Bindestrich (z. B. `2026000123`) – auch im - Posteingang, unabhängig von einem im Template separat hinterlegten ASN-Feld. +- ASN = die ASN, die das Dokument beim Upload **tatsächlich bekäme**: im + 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 ein** Dokument zeigen. Zeigen CF7- und ASN-Treffer auf verschiedene Dokumente diff --git a/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.spec.ts b/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.spec.ts index bfb99b5..068b7dd 100644 --- a/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.spec.ts +++ b/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.spec.ts @@ -1,6 +1,9 @@ import { BadRequestException } from '@nestjs/common'; import { InboxPostprocessorService } from './inbox-postprocessor.service'; -import { originalSeitenDesAbschnitts } from './paperless-entscheidung.util'; +import { + ermittleUploadAsn, + originalSeitenDesAbschnitts, +} from './paperless-entscheidung.util'; describe('InboxPostprocessorService.runForDocument', () => { 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]); }); }); + +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(); + }); +}); diff --git a/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts b/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts index 6a1af6c..12105a0 100644 --- a/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts +++ b/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts @@ -26,6 +26,7 @@ import { applyTemplate, buildVariables } from './variable-resolver'; import { DokumentVersionService } from '../paperless/dokument-version.service'; import { aktionenAbEntscheidung, + ermittleUploadAsn, originalSeitenDesAbschnitts, type PaperlessEntscheidung, } from './paperless-entscheidung.util'; @@ -471,12 +472,18 @@ export class InboxPostprocessorService { if (!interneBelegnummer) 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') { await this.dokumentVersion.haengeVersionAn( entscheidung.documentId, pdfPath, interneBelegnummer, + archiveSerialNumber ?? null, ); this.logger.log( `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) - const asnTpl = String(content.asn ?? '').trim(); - const asn = asnTpl ? applyTemplate(asnTpl, variables).trim() : null; - let archiveSerialNumber: number | undefined; - 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); + // 4. Duplikat-Check Paperless: CF7 + Upload-ASN + const pruefung = await this.paperlessService.pruefeBelegnummer( + interneBelegnummer, + archiveSerialNumber ?? null, + ); if (pruefung.status !== 'frei') { this.logger.warn( `Duplikat (Paperless): Belegnummer ${interneBelegnummer} bereits vorhanden (Dokument(e) ${pruefung.kandidaten.join(', ')})`, diff --git a/paperless-backend/src/inbox-postprocessor/paperless-entscheidung.util.ts b/paperless-backend/src/inbox-postprocessor/paperless-entscheidung.util.ts index 8266b86..2de0b37 100644 --- a/paperless-backend/src/inbox-postprocessor/paperless-entscheidung.util.ts +++ b/paperless-backend/src/inbox-postprocessor/paperless-entscheidung.util.ts @@ -27,3 +27,19 @@ export function originalSeitenDesAbschnitts( ): number[] { 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; +} diff --git a/paperless-backend/src/paperless/dokument-version.service.spec.ts b/paperless-backend/src/paperless/dokument-version.service.spec.ts index 20e2a52..69cac9f 100644 --- a/paperless-backend/src/paperless/dokument-version.service.spec.ts +++ b/paperless-backend/src/paperless/dokument-version.service.spec.ts @@ -56,6 +56,17 @@ describe('DokumentVersionService.haengeVersionAn', () => { 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 () => { paperless.getDocument.mockResolvedValue({ id: 42, tags: [] }); paperless.getTask diff --git a/paperless-backend/src/paperless/dokument-version.service.ts b/paperless-backend/src/paperless/dokument-version.service.ts index 571836a..58378ba 100644 --- a/paperless-backend/src/paperless/dokument-version.service.ts +++ b/paperless-backend/src/paperless/dokument-version.service.ts @@ -32,8 +32,12 @@ export class DokumentVersionService { rootDocumentId: number, pdfPath: string, belegnummer: string, + uploadAsn?: number | null, ): Promise { - const pruefung = await this.paperlessService.pruefeBelegnummer(belegnummer); + const pruefung = await this.paperlessService.pruefeBelegnummer( + belegnummer, + uploadAsn, + ); if ( pruefung.status !== 'eindeutig' || pruefung.documentId !== rootDocumentId diff --git a/paperless-backend/src/paperless/paperless-belegnummer-suche.spec.ts b/paperless-backend/src/paperless/paperless-belegnummer-suche.spec.ts index 29983ce..bdf4a96 100644 --- a/paperless-backend/src/paperless/paperless-belegnummer-suche.spec.ts +++ b/paperless-backend/src/paperless/paperless-belegnummer-suche.spec.ts @@ -85,4 +85,17 @@ describe('PaperlessService – Belegnummer-Suche', () => { 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 }][] + ) + .map(([, cfg]) => cfg.params.archive_serial_number) + .filter((wert) => wert !== undefined); + expect(asnWerte).toEqual([555]); + }); }); diff --git a/paperless-backend/src/paperless/paperless.service.ts b/paperless-backend/src/paperless/paperless.service.ts index 90a856c..9a882bc 100644 --- a/paperless-backend/src/paperless/paperless.service.ts +++ b/paperless-backend/src/paperless/paperless.service.ts @@ -500,12 +500,16 @@ export class PaperlessService { /** * 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 { + async pruefeBelegnummer( + belegnummer: string, + uploadAsn?: number | null, + ): Promise { const nummer = (belegnummer ?? '').trim(); if (!nummer) return klassifiziereTreffer([], []); - const asn = belegnummerZuAsn(nummer); + const asn = uploadAsn ?? belegnummerZuAsn(nummer); const [cf7, asnTreffer] = await Promise.all([ this.findRootDocumentIdsByCustomField(7, nummer), asn !== null