fix(agrarmonitor): gebuchte Belege wieder erkennen, Titel per Template
Build and Push Multi-Platform Images / build-and-push (push) Successful in 18s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 18s
- Connector auf 9bda65e: Buchungsstatus aus #bookedStatus lesen. AM zeigt hinter dem Status jetzt „Zahlung erfassen“, dadurch war buchungsDatum immer leer und das Polling hat keinen Beleg als verbucht markiert. - Polling: Schritt-Logging pro Dokument über AGRARMONITOR_POLLING_DEBUG, Abschlusszeile zählt „noch nicht gebucht“ mit. - Upload-Check: Titel über das Titel-Template des Dokumenttyps, Fallback auf ERG/EGU <Belegnr>. Template-Auflösung als resolveTitelTemplate() ausgelagert und im Processor wiederverwendet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df50470e53
commit
bc2d1cd4b7
8 files changed
+208
-65
No files matched your search
@@ -68,6 +68,7 @@ AGRARMONITOR_COOKIE_PATH=./data/agrarmonitor-cookies.json
|
|||||||
AGRARMONITOR_ENCRYPTION_KEY= # optional, 16+ Zeichen für Cookie-Verschlüsselung
|
AGRARMONITOR_ENCRYPTION_KEY= # optional, 16+ Zeichen für Cookie-Verschlüsselung
|
||||||
AGRARMONITOR_POLLING_CRON=0 */30 * * * * # Polling-Intervall (Standard: alle 30 Minuten); leer lassen zum Deaktivieren
|
AGRARMONITOR_POLLING_CRON=0 */30 * * * * # Polling-Intervall (Standard: alle 30 Minuten); leer lassen zum Deaktivieren
|
||||||
AGRARMONITOR_UPLOAD_CHECK_CRON=0 * * * * * # Upload-Check-Intervall (Standard: einmal pro Minute); leer lassen zum Deaktivieren
|
AGRARMONITOR_UPLOAD_CHECK_CRON=0 * * * * * # Upload-Check-Intervall (Standard: einmal pro Minute); leer lassen zum Deaktivieren
|
||||||
|
AGRARMONITOR_POLLING_DEBUG= # true = jeden Schritt des Polling-Laufs pro Dokument loggen
|
||||||
|
|
||||||
# --- Täglicher Digest ---
|
# --- Täglicher Digest ---
|
||||||
# Basis-URL der App für klickbare Links in Digest-E-Mails (z.B. https://paperless.example.com)
|
# Basis-URL der App für klickbare Links in Digest-E-Mails (z.B. https://paperless.example.com)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ services:
|
|||||||
- AGRARMONITOR_ENCRYPTION_KEY=${AGRARMONITOR_ENCRYPTION_KEY:-}
|
- AGRARMONITOR_ENCRYPTION_KEY=${AGRARMONITOR_ENCRYPTION_KEY:-}
|
||||||
- AGRARMONITOR_POLLING_CRON=${AGRARMONITOR_POLLING_CRON:-}
|
- AGRARMONITOR_POLLING_CRON=${AGRARMONITOR_POLLING_CRON:-}
|
||||||
- AGRARMONITOR_UPLOAD_CHECK_CRON=${AGRARMONITOR_UPLOAD_CHECK_CRON:-}
|
- AGRARMONITOR_UPLOAD_CHECK_CRON=${AGRARMONITOR_UPLOAD_CHECK_CRON:-}
|
||||||
|
- AGRARMONITOR_POLLING_DEBUG=${AGRARMONITOR_POLLING_DEBUG:-}
|
||||||
- APP_URL=${APP_URL:-}
|
- APP_URL=${APP_URL:-}
|
||||||
- DAILY_DIGEST_CRON=${DAILY_DIGEST_CRON:-}
|
- DAILY_DIGEST_CRON=${DAILY_DIGEST_CRON:-}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Generated
+1
-1
@@ -5064,7 +5064,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/agrarmonitor-connector": {
|
"node_modules/agrarmonitor-connector": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#e6ce1d254bf21522b73a9323bb8f8cd9fadd7eef",
|
"resolved": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#9bda65eec0ec45c98ecce506fe68e36991c0ae61",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { PaperlessService } from '../paperless/paperless.service';
|
|||||||
import { Setting } from '../database/entities/setting.entity';
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
import { Client } from '../database/entities/client.entity';
|
import { Client } from '../database/entities/client.entity';
|
||||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||||
|
import { DocumentType } from '../database/entities/document-type.entity';
|
||||||
|
import { resolveTitelTemplate } from '../paperless/titel-template.util';
|
||||||
|
|
||||||
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
||||||
const EINGANGSDATUM_FIELD_ID = 9;
|
const EINGANGSDATUM_FIELD_ID = 9;
|
||||||
@@ -47,6 +49,10 @@ export interface SyncCorrespondentsResult {
|
|||||||
export class AgrarmonitorPollingService implements OnModuleInit {
|
export class AgrarmonitorPollingService implements OnModuleInit {
|
||||||
private readonly logger = new Logger(AgrarmonitorPollingService.name);
|
private readonly logger = new Logger(AgrarmonitorPollingService.name);
|
||||||
private pollingRunning = false;
|
private pollingRunning = false;
|
||||||
|
/** Schritt-Logging für runPolling(), aktivierbar über AGRARMONITOR_POLLING_DEBUG=true */
|
||||||
|
private readonly debug = ['1', 'true', 'yes'].includes(
|
||||||
|
(process.env['AGRARMONITOR_POLLING_DEBUG'] ?? '').toLowerCase(),
|
||||||
|
);
|
||||||
private uploadCheckRunning = false;
|
private uploadCheckRunning = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -57,6 +63,8 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
||||||
@InjectRepository(CorrespondentSetting)
|
@InjectRepository(CorrespondentSetting)
|
||||||
private readonly corrSettingRepo: Repository<CorrespondentSetting>,
|
private readonly corrSettingRepo: Repository<CorrespondentSetting>,
|
||||||
|
@InjectRepository(DocumentType)
|
||||||
|
private readonly docTypeRepo: Repository<DocumentType>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
@@ -210,6 +218,10 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
const tagFertigId = parseInt(tagFertigSetting?.Wert ?? '4', 10);
|
const tagFertigId = parseInt(tagFertigSetting?.Wert ?? '4', 10);
|
||||||
const tagVerbuchtId = parseInt(tagVerbuchtSetting?.Wert ?? '9', 10);
|
const tagVerbuchtId = parseInt(tagVerbuchtSetting?.Wert ?? '9', 10);
|
||||||
|
|
||||||
|
this.trace(
|
||||||
|
`Konfiguration: Tag fertig=${tagFertigId}, Tag verbucht=${tagVerbuchtId}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (isNaN(tagFertigId) || isNaN(tagVerbuchtId)) {
|
if (isNaN(tagFertigId) || isNaN(tagVerbuchtId)) {
|
||||||
const msg = 'Tag-IDs ungültig (keine Zahlen)';
|
const msg = 'Tag-IDs ungültig (keine Zahlen)';
|
||||||
this.logger.error(msg);
|
this.logger.error(msg);
|
||||||
@@ -236,9 +248,13 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
return { ...result, errors: [msg] };
|
return { ...result, errors: [msg] };
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const customer of customers.filter(
|
const aktiveLieferanten = customers.filter(
|
||||||
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
||||||
)) {
|
);
|
||||||
|
this.trace(
|
||||||
|
`${customers.length} Kunden geladen, davon ${aktiveLieferanten.length} aktive Lieferanten — starte Korrespondenten-Sync`,
|
||||||
|
);
|
||||||
|
for (const customer of aktiveLieferanten) {
|
||||||
try {
|
try {
|
||||||
await this.getOrCreateCorrespondent(customer, Number(customer.id));
|
await this.getOrCreateCorrespondent(customer, Number(customer.id));
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -261,9 +277,14 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.logger.log(`${docs.length} Dokumente fertig in Agrarmonitor`);
|
this.logger.log(`${docs.length} Dokumente fertig in Agrarmonitor`);
|
||||||
|
this.trace(
|
||||||
|
`Paperless meldet ${docsResponse?.count ?? 0} Dokumente mit Tag ${tagFertigId}`,
|
||||||
|
);
|
||||||
|
let nichtGebucht = 0;
|
||||||
|
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
result.processed++;
|
result.processed++;
|
||||||
|
const nr = `[${result.processed}/${docs.length}] Dokument ${doc.id as number}`;
|
||||||
|
|
||||||
const interneBelegnummer =
|
const interneBelegnummer =
|
||||||
(((doc.custom_fields as any[]) ?? []).find(
|
(((doc.custom_fields as any[]) ?? []).find(
|
||||||
@@ -279,6 +300,10 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.trace(
|
||||||
|
`${nr} „${doc.title as string}“: interne Belegnummer ${interneBelegnummer}, Tags [${((doc.tags as number[]) ?? []).join(', ')}] — starte Livesearch`,
|
||||||
|
);
|
||||||
|
|
||||||
let amResults: Awaited<
|
let amResults: Awaited<
|
||||||
ReturnType<typeof amClient.eingangsrechnungenLivesearch>
|
ReturnType<typeof amClient.eingangsrechnungenLivesearch>
|
||||||
>;
|
>;
|
||||||
@@ -295,14 +320,17 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||||
this.logger.error(
|
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||||
`${msg}: ${fehlerText(err)}`,
|
|
||||||
);
|
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
await this.delay(500);
|
await this.delay(500);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.trace(
|
||||||
|
`${nr}: Livesearch lieferte ${amResults.length} Treffer` +
|
||||||
|
amResults.map((r) => `\n ${this.describeAmDoc(r)}`).join(''),
|
||||||
|
);
|
||||||
|
|
||||||
if (amResults.length === 0) {
|
if (amResults.length === 0) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`${interneBelegnummer} nicht in Agrarmonitor gefunden`,
|
`${interneBelegnummer} nicht in Agrarmonitor gefunden`,
|
||||||
@@ -323,6 +351,9 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
const amDoc = amResults[0];
|
const amDoc = amResults[0];
|
||||||
|
|
||||||
if (!amDoc.interneBelegNummer && interneBelegnummer) {
|
if (!amDoc.interneBelegNummer && interneBelegnummer) {
|
||||||
|
this.trace(
|
||||||
|
`${nr}: Lieferscheinnummer in AM leer — setze ${interneBelegnummer}`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await amClient.setLieferscheinNummer(
|
await amClient.setLieferscheinNummer(
|
||||||
amDoc.eingangId,
|
amDoc.eingangId,
|
||||||
@@ -339,6 +370,9 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
const eingangsdatumField = ((doc.custom_fields as any[]) ?? []).find(
|
const eingangsdatumField = ((doc.custom_fields as any[]) ?? []).find(
|
||||||
(cf: any) => cf.field === EINGANGSDATUM_FIELD_ID,
|
(cf: any) => cf.field === EINGANGSDATUM_FIELD_ID,
|
||||||
);
|
);
|
||||||
|
this.trace(
|
||||||
|
`${nr}: Eingangsdatum in AM leer — Paperless-Feld ${EINGANGSDATUM_FIELD_ID} = ${JSON.stringify(eingangsdatumField?.value ?? null)}`,
|
||||||
|
);
|
||||||
if (eingangsdatumField?.value) {
|
if (eingangsdatumField?.value) {
|
||||||
const eingangsdatum = new Date(eingangsdatumField.value as string);
|
const eingangsdatum = new Date(eingangsdatumField.value as string);
|
||||||
if (!isNaN(eingangsdatum.getTime())) {
|
if (!isNaN(eingangsdatum.getTime())) {
|
||||||
@@ -350,7 +384,17 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!amDoc.buchungsDatum) {
|
||||||
|
nichtGebucht++;
|
||||||
|
this.trace(
|
||||||
|
`${nr}: kein Buchungsdatum in AM — noch nicht gebucht, keine Änderung`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (amDoc.buchungsDatum) {
|
if (amDoc.buchungsDatum) {
|
||||||
|
this.trace(
|
||||||
|
`${nr}: gebucht am ${this.fmtDate(amDoc.buchungsDatum)} — aktualisiere Paperless`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
let correspondentId: number | undefined;
|
let correspondentId: number | undefined;
|
||||||
const customer = customers.find(
|
const customer = customers.find(
|
||||||
@@ -384,6 +428,9 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
updateData.correspondent = correspondentId;
|
updateData.correspondent = correspondentId;
|
||||||
if (ownerId !== undefined) updateData.owner = ownerId;
|
if (ownerId !== undefined) updateData.owner = ownerId;
|
||||||
|
|
||||||
|
this.trace(
|
||||||
|
`${nr}: Kunde ${amDoc.kundenId} → Korrespondent ${correspondentId ?? '–'}, Betrieb ${amDoc.betriebId} → Owner ${ownerId ?? '–'}, Update ${JSON.stringify(updateData)}`,
|
||||||
|
);
|
||||||
await this.paperlessService.updateDocument(
|
await this.paperlessService.updateDocument(
|
||||||
doc.id as number,
|
doc.id as number,
|
||||||
updateData,
|
updateData,
|
||||||
@@ -392,9 +439,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
result.updated++;
|
result.updated++;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = `${interneBelegnummer}: Update-Fehler`;
|
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||||
this.logger.error(
|
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||||
`${msg}: ${fehlerText(err)}`,
|
|
||||||
);
|
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,7 +449,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Polling abgeschlossen: ${result.processed} verarbeitet, ${result.updated} aktualisiert, ` +
|
`Polling abgeschlossen: ${result.processed} verarbeitet, ${result.updated} aktualisiert, ` +
|
||||||
`${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
`${nichtGebucht} noch nicht gebucht, ${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.pollingRunning = false;
|
this.pollingRunning = false;
|
||||||
@@ -525,9 +570,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const msg = `${interneBelegnummer}: Vorhanden-Check fehlgeschlagen`;
|
const msg = `${interneBelegnummer}: Vorhanden-Check fehlgeschlagen`;
|
||||||
this.logger.error(
|
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||||
`${msg}: ${fehlerText(err)}`,
|
|
||||||
);
|
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
await this.delay(500);
|
await this.delay(500);
|
||||||
continue;
|
continue;
|
||||||
@@ -552,9 +595,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
// Bei Fehler vorsichtig: nicht verschieben
|
// Bei Fehler vorsichtig: nicht verschieben
|
||||||
const msg = `${interneBelegnummer}: Dateieingang-Check fehlgeschlagen`;
|
const msg = `${interneBelegnummer}: Dateieingang-Check fehlgeschlagen`;
|
||||||
this.logger.error(
|
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||||
`${msg}: ${fehlerText(err)}`,
|
|
||||||
);
|
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
await this.delay(500);
|
await this.delay(500);
|
||||||
continue;
|
continue;
|
||||||
@@ -645,9 +686,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||||
this.logger.error(
|
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||||
`${msg}: ${fehlerText(err)}`,
|
|
||||||
);
|
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
await this.delay(500);
|
await this.delay(500);
|
||||||
continue;
|
continue;
|
||||||
@@ -718,10 +757,23 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Titel über das Titel-Template des Dokumenttyps bilden;
|
||||||
|
// ohne (auflösbares) Template bleibt der bisherige Standardtitel.
|
||||||
|
const documentTypeId = amDoc.dokumentTyp === 0 ? 1 : 2;
|
||||||
|
const docTypeConfig = await this.docTypeRepo.findOneBy({
|
||||||
|
DocumentTypeId: documentTypeId,
|
||||||
|
});
|
||||||
|
const title =
|
||||||
|
resolveTitelTemplate(
|
||||||
|
docTypeConfig?.TitelTemplate,
|
||||||
|
existingFields,
|
||||||
|
amDoc.belegDatum ?? new Date(doc.created as string),
|
||||||
|
) ??
|
||||||
|
(amDoc.dokumentTyp === 0 ? 'ERG ' : 'EGU ') + amDoc.belegNummer;
|
||||||
|
|
||||||
const updateData: Record<string, any> = {
|
const updateData: Record<string, any> = {
|
||||||
title:
|
title,
|
||||||
(amDoc.dokumentTyp === 0 ? 'ERG ' : 'EGU ') + amDoc.belegNummer,
|
document_type: documentTypeId,
|
||||||
document_type: amDoc.dokumentTyp === 0 ? 1 : 2,
|
|
||||||
tags: newTags,
|
tags: newTags,
|
||||||
custom_fields: existingFields,
|
custom_fields: existingFields,
|
||||||
};
|
};
|
||||||
@@ -742,9 +794,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
result.updated++;
|
result.updated++;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = `${interneBelegnummer}: Update-Fehler`;
|
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||||
this.logger.error(
|
this.logger.error(`${msg}: ${fehlerText(err)}`);
|
||||||
`${msg}: ${fehlerText(err)}`,
|
|
||||||
);
|
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,6 +1068,34 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
return `${name} (${nummer})`;
|
return `${name} (${nummer})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private trace(message: string): void {
|
||||||
|
if (this.debug) this.logger.log(`[Debug] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fmtDate(d: Date | null | undefined): string {
|
||||||
|
if (!d) return '–';
|
||||||
|
return isNaN(d.getTime()) ? 'ungültig' : d.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private describeAmDoc(r: {
|
||||||
|
eingangId: number;
|
||||||
|
belegNummer: string;
|
||||||
|
interneBelegNummer: string;
|
||||||
|
belegDatum: Date | null;
|
||||||
|
eingangsDatum: Date | null;
|
||||||
|
buchungsDatum: Date | null;
|
||||||
|
kundenId: number;
|
||||||
|
betriebId: number;
|
||||||
|
dokumentTyp: number;
|
||||||
|
}): string {
|
||||||
|
return (
|
||||||
|
`eingangId=${r.eingangId} beleg=${r.belegNummer || '–'} intern=${r.interneBelegNummer || '–'} ` +
|
||||||
|
`typ=${r.dokumentTyp} belegDatum=${this.fmtDate(r.belegDatum)} ` +
|
||||||
|
`eingangsDatum=${this.fmtDate(r.eingangsDatum)} buchungsDatum=${this.fmtDate(r.buchungsDatum)} ` +
|
||||||
|
`kunde=${r.kundenId} betrieb=${r.betriebId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private delay(ms: number): Promise<void> {
|
private delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ import { PaperlessModule } from '../paperless/paperless.module';
|
|||||||
import { Setting } from '../database/entities/setting.entity';
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
import { Client } from '../database/entities/client.entity';
|
import { Client } from '../database/entities/client.entity';
|
||||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||||
|
import { DocumentType } from '../database/entities/document-type.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Setting, Client, CorrespondentSetting]),
|
TypeOrmModule.forFeature([
|
||||||
|
Setting,
|
||||||
|
Client,
|
||||||
|
CorrespondentSetting,
|
||||||
|
DocumentType,
|
||||||
|
]),
|
||||||
PaperlessModule,
|
PaperlessModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
|||||||
import { DocumentField } from '../database/entities/document-field.entity';
|
import { DocumentField } from '../database/entities/document-field.entity';
|
||||||
import { DocumentType } from '../database/entities/document-type.entity';
|
import { DocumentType } from '../database/entities/document-type.entity';
|
||||||
import { PaperlessService } from './paperless.service';
|
import { PaperlessService } from './paperless.service';
|
||||||
|
import { resolveTitelTemplate } from './titel-template.util';
|
||||||
import { PostprocessingService } from '../postprocessing/postprocessing.service';
|
import { PostprocessingService } from '../postprocessing/postprocessing.service';
|
||||||
|
|
||||||
const PAPERLESSMANAGER_TAG_ID = 16; // Tag "paperlessmanager"
|
const PAPERLESSMANAGER_TAG_ID = 16; // Tag "paperlessmanager"
|
||||||
@@ -220,45 +221,12 @@ export class PaperlessProcessorService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (docTypeConfig.TitelTemplate) {
|
if (docTypeConfig.TitelTemplate) {
|
||||||
let title = docTypeConfig.TitelTemplate;
|
const title = resolveTitelTemplate(
|
||||||
|
docTypeConfig.TitelTemplate,
|
||||||
// Prüfen ob alle im Template referenzierten Custom Fields ausgefüllt sind
|
newCustomFields,
|
||||||
const placeholderRegex = /\{\{CUSTOM\[(\d+)\]\}\}/g;
|
doc.created ? new Date(doc.created) : new Date(),
|
||||||
let allFilled = true;
|
);
|
||||||
let match: RegExpExecArray | null;
|
if (title !== null) {
|
||||||
|
|
||||||
while ((match = placeholderRegex.exec(title)) !== null) {
|
|
||||||
const fieldId = parseInt(match[1], 10);
|
|
||||||
const cf = newCustomFields.find((f) => f.field === fieldId);
|
|
||||||
if (!cf || cf.value == null || cf.value === '') {
|
|
||||||
allFilled = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allFilled) {
|
|
||||||
for (const cf of newCustomFields) {
|
|
||||||
const placeholder = `{{CUSTOM[${cf.field}]}}`;
|
|
||||||
if (title.includes(placeholder)) {
|
|
||||||
title = title.replaceAll(
|
|
||||||
placeholder,
|
|
||||||
cf.value != null ? String(cf.value) : '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('{{DATE}}')) {
|
|
||||||
const created = doc.created ? new Date(doc.created) : new Date();
|
|
||||||
const dateStr = `${String(created.getDate()).padStart(2, '0')}.${String(created.getMonth() + 1).padStart(2, '0')}.${created.getFullYear()}`;
|
|
||||||
title = title.replaceAll('{{DATE}}', dateStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title.includes('{{ZEITSTEMPEL}}')) {
|
|
||||||
const now = new Date();
|
|
||||||
const ts = `${String(now.getDate()).padStart(2, '0')}.${String(now.getMonth() + 1).padStart(2, '0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
||||||
title = title.replaceAll('{{ZEITSTEMPEL}}', ts);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePayload.title = title;
|
updatePayload.title = title;
|
||||||
} else {
|
} else {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { resolveTitelTemplate } from './titel-template.util';
|
||||||
|
|
||||||
|
describe('resolveTitelTemplate', () => {
|
||||||
|
const created = new Date(2026, 9, 5); // 05.10.2026
|
||||||
|
|
||||||
|
it('ersetzt Custom-Field-Platzhalter und {{DATE}}', () => {
|
||||||
|
expect(
|
||||||
|
resolveTitelTemplate(
|
||||||
|
'ERG {{CUSTOM[3]}} vom {{DATE}}',
|
||||||
|
[{ field: 3, value: 'RE-42' }],
|
||||||
|
created,
|
||||||
|
),
|
||||||
|
).toBe('ERG RE-42 vom 05.10.2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ersetzt mehrfach vorkommende Platzhalter', () => {
|
||||||
|
expect(
|
||||||
|
resolveTitelTemplate(
|
||||||
|
'{{CUSTOM[3]}}/{{CUSTOM[3]}}',
|
||||||
|
[{ field: 3, value: 'X' }],
|
||||||
|
created,
|
||||||
|
),
|
||||||
|
).toBe('X/X');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('liefert null, wenn ein referenziertes Feld leer ist oder fehlt', () => {
|
||||||
|
expect(
|
||||||
|
resolveTitelTemplate('{{CUSTOM[3]}}', [{ field: 3, value: '' }], created),
|
||||||
|
).toBeNull();
|
||||||
|
expect(resolveTitelTemplate('{{CUSTOM[7]}}', [], created)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('liefert null bei leerem Template', () => {
|
||||||
|
expect(resolveTitelTemplate('', [], created)).toBeNull();
|
||||||
|
expect(resolveTitelTemplate(null, [], created)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ersetzt {{ZEITSTEMPEL}} mit der übergebenen Uhrzeit', () => {
|
||||||
|
expect(
|
||||||
|
resolveTitelTemplate(
|
||||||
|
'T {{ZEITSTEMPEL}}',
|
||||||
|
[],
|
||||||
|
created,
|
||||||
|
new Date(2026, 0, 2, 3, 4, 5),
|
||||||
|
),
|
||||||
|
).toBe('T 02.01.2026 03:04:05');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
|
||||||
|
const formatDatum = (d: Date) =>
|
||||||
|
`${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst ein Titel-Template eines Dokumenttyps auf.
|
||||||
|
* Platzhalter: {{CUSTOM[<feldId>]}}, {{DATE}} (Belegdatum), {{ZEITSTEMPEL}} (jetzt).
|
||||||
|
* Liefert null, wenn kein Template gesetzt ist oder ein referenziertes
|
||||||
|
* Custom Field leer ist — der Aufrufer behält dann den bisherigen Titel.
|
||||||
|
*/
|
||||||
|
export function resolveTitelTemplate(
|
||||||
|
template: string | null | undefined,
|
||||||
|
customFields: Array<{ field: number; value: unknown }>,
|
||||||
|
created: Date,
|
||||||
|
now: Date = new Date(),
|
||||||
|
): string | null {
|
||||||
|
if (!template) return null;
|
||||||
|
|
||||||
|
const placeholderRegex = /\{\{CUSTOM\[(\d+)\]\}\}/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = placeholderRegex.exec(template)) !== null) {
|
||||||
|
const fieldId = parseInt(match[1], 10);
|
||||||
|
const cf = customFields.find((f) => f.field === fieldId);
|
||||||
|
if (!cf || cf.value == null || cf.value === '') return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = template;
|
||||||
|
for (const cf of customFields) {
|
||||||
|
title = title.replaceAll(
|
||||||
|
`{{CUSTOM[${cf.field}]}}`,
|
||||||
|
cf.value != null ? String(cf.value as string | number | boolean) : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
title = title.replaceAll('{{DATE}}', formatDatum(created));
|
||||||
|
title = title.replaceAll(
|
||||||
|
'{{ZEITSTEMPEL}}',
|
||||||
|
`${formatDatum(now)} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
|
||||||
|
);
|
||||||
|
return title;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user