refactor: replace AgrarmonitorWebService with connector methods
- Delete agrarmonitor-web.service.ts (HTML-scraping no longer needed) - Rewrite AgrarmonitorPollingService to call connector directly (eingangsrechnungenLivesearch, setEingangsdatum, setLieferscheinNummer) - Fix quality issues: concurrency guard, customer-sync try/catch, tag dedup via Set, parseInt NaN guard, page_size overflow warning - Update AgrarmonitorModule to import TypeORM/PaperlessModule - Remove node-html-parser dependency - Update agrarmonitor-connector to latest Gitea commit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-58
@@ -30,7 +30,6 @@
|
|||||||
"jwks-rsa": "^4.0.1",
|
"jwks-rsa": "^4.0.1",
|
||||||
"mailparser": "^3.9.8",
|
"mailparser": "^3.9.8",
|
||||||
"mysql2": "^3.20.0",
|
"mysql2": "^3.20.0",
|
||||||
"node-html-parser": "^7.1.0",
|
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@@ -4863,7 +4862,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#921c67503b68e46d504ac1c72fb1372cda633006",
|
"resolved": "git+https://gitea.poettker-cloud.de/bjoernpoettker/AgrarmonitorConnector.git#cf6bc1b5cc7e5ffa060c4a37bcea7d9ea6635527",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
@@ -5337,12 +5336,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/boolbase": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
@@ -6024,22 +6017,6 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-select": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"boolbase": "^1.0.0",
|
|
||||||
"css-what": "^6.1.0",
|
|
||||||
"domhandler": "^5.0.2",
|
|
||||||
"domutils": "^3.0.1",
|
|
||||||
"nth-check": "^2.0.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/css-tree": {
|
"node_modules/css-tree": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
@@ -6053,18 +6030,6 @@
|
|||||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-what": {
|
|
||||||
"version": "6.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
|
||||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
@@ -9976,16 +9941,6 @@
|
|||||||
"url": "https://opencollective.com/node-fetch"
|
"url": "https://opencollective.com/node-fetch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-html-parser": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"css-select": "^5.1.0",
|
|
||||||
"he": "1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@@ -10032,18 +9987,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nth-check": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"boolbase": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
"jwks-rsa": "^4.0.1",
|
"jwks-rsa": "^4.0.1",
|
||||||
"mailparser": "^3.9.8",
|
"mailparser": "^3.9.8",
|
||||||
"mysql2": "^3.20.0",
|
"mysql2": "^3.20.0",
|
||||||
"node-html-parser": "^7.1.0",
|
|
||||||
"nodemailer": "^8.0.5",
|
"nodemailer": "^8.0.5",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
// paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts
|
|
||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AgrarmonitorService } from './agrarmonitor.service';
|
import { AgrarmonitorService } from './agrarmonitor.service';
|
||||||
import { AgrarmonitorWebService } from './agrarmonitor-web.service';
|
|
||||||
import { PaperlessService } from '../paperless/paperless.service';
|
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';
|
||||||
|
|
||||||
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
||||||
const EINGANGSDATUM_FIELD_ID = 9;
|
const EINGANGSDATUM_FIELD_ID = 9;
|
||||||
|
const DOCS_PAGE_SIZE = 500;
|
||||||
|
|
||||||
export interface PollingResult {
|
export interface PollingResult {
|
||||||
processed: number;
|
processed: number;
|
||||||
@@ -22,10 +21,10 @@ export interface PollingResult {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
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;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly agrarmonitorService: AgrarmonitorService,
|
private readonly agrarmonitorService: AgrarmonitorService,
|
||||||
private readonly webService: AgrarmonitorWebService,
|
|
||||||
private readonly paperlessService: PaperlessService,
|
private readonly paperlessService: PaperlessService,
|
||||||
@InjectRepository(Setting) private readonly settingRepo: Repository<Setting>,
|
@InjectRepository(Setting) private readonly settingRepo: Repository<Setting>,
|
||||||
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
||||||
@@ -60,172 +59,201 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async runPolling(): Promise<PollingResult> {
|
async runPolling(): Promise<PollingResult> {
|
||||||
|
if (this.pollingRunning) {
|
||||||
|
this.logger.warn('Polling läuft bereits, überspringe');
|
||||||
|
return { processed: 0, updated: 0, skipped: 0, errors: ['Polling bereits aktiv'] };
|
||||||
|
}
|
||||||
|
this.pollingRunning = true;
|
||||||
|
|
||||||
const result: PollingResult = { processed: 0, updated: 0, skipped: 0, errors: [] };
|
const result: PollingResult = { processed: 0, updated: 0, skipped: 0, errors: [] };
|
||||||
this.logger.log('Starte Agrarmonitor-Polling');
|
this.logger.log('Starte Agrarmonitor-Polling');
|
||||||
|
|
||||||
const [tagFertigSetting, tagVerbuchtSetting] = await Promise.all([
|
|
||||||
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
|
|
||||||
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }),
|
|
||||||
]);
|
|
||||||
const tagFertigId = parseInt(tagFertigSetting?.Wert ?? '4', 10);
|
|
||||||
const tagVerbuchtId = parseInt(tagVerbuchtSetting?.Wert ?? '9', 10);
|
|
||||||
|
|
||||||
let amClient: Awaited<ReturnType<typeof this.agrarmonitorService.getClient>>;
|
|
||||||
try {
|
try {
|
||||||
amClient = await this.agrarmonitorService.getClient();
|
const [tagFertigSetting, tagVerbuchtSetting] = await Promise.all([
|
||||||
} catch (err: unknown) {
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_fertig' }),
|
||||||
const msg = `Connector-Fehler: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
this.settingRepo.findOneBy({ Tag: 'agrarmonitor_tag_verbucht' }),
|
||||||
this.logger.error(msg);
|
]);
|
||||||
return { ...result, errors: [msg] };
|
const tagFertigId = parseInt(tagFertigSetting?.Wert ?? '4', 10);
|
||||||
}
|
const tagVerbuchtId = parseInt(tagVerbuchtSetting?.Wert ?? '9', 10);
|
||||||
|
|
||||||
let customers: Awaited<ReturnType<typeof amClient.fetchCustomers>>;
|
if (isNaN(tagFertigId) || isNaN(tagVerbuchtId)) {
|
||||||
try {
|
const msg = 'Tag-IDs ungültig (keine Zahlen)';
|
||||||
customers = await amClient.fetchCustomers();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const msg = `Kunden-Abruf fehlgeschlagen: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
|
||||||
this.logger.error(msg);
|
|
||||||
return { ...result, errors: [msg] };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const customer of customers.filter(
|
|
||||||
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
|
||||||
)) {
|
|
||||||
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
|
||||||
const searchName = `(${lieferantennummer})`;
|
|
||||||
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
|
||||||
const existing = await this.paperlessService.getCorrespondentByName(searchName);
|
|
||||||
if (!existing) {
|
|
||||||
await this.paperlessService.addCorrespondent({
|
|
||||||
name: displayName,
|
|
||||||
match: '',
|
|
||||||
matching_algorithm: 0,
|
|
||||||
is_insensitive: true,
|
|
||||||
owner: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const docsResponse = await this.paperlessService.getDocuments({
|
|
||||||
page: 1,
|
|
||||||
page_size: 9999,
|
|
||||||
truncate_content: true,
|
|
||||||
tags__id__all: tagFertigId,
|
|
||||||
});
|
|
||||||
const docs: any[] = docsResponse?.results ?? [];
|
|
||||||
this.logger.log(`${docs.length} Dokumente fertig in Agrarmonitor`);
|
|
||||||
|
|
||||||
for (const doc of docs) {
|
|
||||||
result.processed++;
|
|
||||||
|
|
||||||
const interneBelegnummer =
|
|
||||||
((doc.custom_fields as any[]) ?? []).find(
|
|
||||||
(cf: any) => cf.field === INTERN_BELEGNUMMER_FIELD_ID,
|
|
||||||
)?.value as string ?? '';
|
|
||||||
|
|
||||||
if (!interneBelegnummer) {
|
|
||||||
this.logger.log(`Dokument ${doc.id as number} hat keine interne Belegnummer`);
|
|
||||||
result.skipped++;
|
|
||||||
await this.delay(500);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let amResults: Awaited<ReturnType<typeof this.webService.eingangsrechnungenLivesearch>>;
|
|
||||||
try {
|
|
||||||
amResults = await this.webService.eingangsrechnungenLivesearch(interneBelegnummer);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
|
||||||
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
|
||||||
result.errors.push(msg);
|
|
||||||
await this.delay(500);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amResults.length === 0) {
|
|
||||||
this.logger.log(`${interneBelegnummer} nicht in Agrarmonitor gefunden`);
|
|
||||||
result.skipped++;
|
|
||||||
await this.delay(500);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amResults.length > 1) {
|
|
||||||
const msg = `${interneBelegnummer}: Mehrfach gefunden`;
|
|
||||||
this.logger.error(msg);
|
this.logger.error(msg);
|
||||||
result.errors.push(msg);
|
return { ...result, errors: [msg] };
|
||||||
await this.delay(500);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const amDoc = amResults[0];
|
let amClient: Awaited<ReturnType<typeof this.agrarmonitorService.getClient>>;
|
||||||
|
try {
|
||||||
if (!amDoc.interneBelegNummer && interneBelegnummer) {
|
amClient = await this.agrarmonitorService.getClient();
|
||||||
await this.webService.setLieferscheinNummer(amDoc.eingangId, interneBelegnummer);
|
} catch (err: unknown) {
|
||||||
|
const msg = `Connector-Fehler: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
||||||
|
this.logger.error(msg);
|
||||||
|
return { ...result, errors: [msg] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!amDoc.eingangsDatum) {
|
let customers: Awaited<ReturnType<typeof amClient.fetchCustomers>>;
|
||||||
const eingangsdatumField = ((doc.custom_fields as any[]) ?? []).find(
|
try {
|
||||||
(cf: any) => cf.field === EINGANGSDATUM_FIELD_ID,
|
customers = await amClient.fetchCustomers();
|
||||||
);
|
} catch (err: unknown) {
|
||||||
if (eingangsdatumField?.value) {
|
const msg = `Kunden-Abruf fehlgeschlagen: ${err instanceof Error ? err.message : 'unbekannt'}`;
|
||||||
const eingangsdatum = new Date(eingangsdatumField.value as string);
|
this.logger.error(msg);
|
||||||
if (!isNaN(eingangsdatum.getTime())) {
|
return { ...result, errors: [msg] };
|
||||||
await this.webService.setEingangsdatum(amDoc.eingangId, eingangsdatum);
|
}
|
||||||
this.logger.log(`Eingangsdatum für ${interneBelegnummer} gesetzt`);
|
|
||||||
}
|
for (const customer of customers.filter(
|
||||||
}
|
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
||||||
} else if (amDoc.buchungsDatum) {
|
)) {
|
||||||
try {
|
try {
|
||||||
let correspondentId: number | undefined;
|
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||||
const customer = customers.find((c) => Number(c.id) === amDoc.kundenId);
|
const searchName = `(${lieferantennummer})`;
|
||||||
if (customer) {
|
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
||||||
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
const existing = await this.paperlessService.getCorrespondentByName(searchName);
|
||||||
const searchName = `(${lieferantennummer})`;
|
if (!existing) {
|
||||||
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
await this.paperlessService.addCorrespondent({
|
||||||
let corr = await this.paperlessService.getCorrespondentByName(searchName);
|
name: displayName,
|
||||||
if (!corr) {
|
match: '',
|
||||||
corr = await this.paperlessService.addCorrespondent({
|
matching_algorithm: 0,
|
||||||
name: displayName,
|
is_insensitive: true,
|
||||||
match: '',
|
owner: null,
|
||||||
matching_algorithm: 0,
|
});
|
||||||
is_insensitive: true,
|
|
||||||
owner: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (corr) correspondentId = corr.id as number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ownerId: number | undefined;
|
|
||||||
const matchedClient = await this.clientRepo.findOneBy({
|
|
||||||
AgrarmonitorBetriebId: amDoc.betriebId,
|
|
||||||
});
|
|
||||||
if (matchedClient) ownerId = matchedClient.PaperlessUserId;
|
|
||||||
|
|
||||||
const currentTags: number[] = (doc.tags as number[]) ?? [];
|
|
||||||
const newTags = currentTags.filter((t) => t !== tagFertigId).concat([tagVerbuchtId]);
|
|
||||||
|
|
||||||
const updateData: Record<string, any> = { tags: newTags };
|
|
||||||
if (correspondentId !== undefined) updateData.correspondent = correspondentId;
|
|
||||||
if (ownerId !== undefined) updateData.owner = ownerId;
|
|
||||||
|
|
||||||
await this.paperlessService.updateDocument(doc.id as number, updateData);
|
|
||||||
this.logger.log(`Beleg ${interneBelegnummer} gebucht`);
|
|
||||||
result.updated++;
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = `${interneBelegnummer}: Update-Fehler`;
|
this.logger.warn(`Korrespondenten-Sync fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsResponse = await this.paperlessService.getDocuments({
|
||||||
|
page: 1,
|
||||||
|
page_size: DOCS_PAGE_SIZE,
|
||||||
|
truncate_content: true,
|
||||||
|
tags__id__all: tagFertigId,
|
||||||
|
});
|
||||||
|
const docs: any[] = docsResponse?.results ?? [];
|
||||||
|
if ((docsResponse?.count ?? 0) > DOCS_PAGE_SIZE) {
|
||||||
|
this.logger.warn(`Mehr als ${DOCS_PAGE_SIZE} Dokumente bereit — nur erste ${DOCS_PAGE_SIZE} werden verarbeitet`);
|
||||||
|
}
|
||||||
|
this.logger.log(`${docs.length} Dokumente fertig in Agrarmonitor`);
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
result.processed++;
|
||||||
|
|
||||||
|
const interneBelegnummer =
|
||||||
|
((doc.custom_fields as any[]) ?? []).find(
|
||||||
|
(cf: any) => cf.field === INTERN_BELEGNUMMER_FIELD_ID,
|
||||||
|
)?.value as string ?? '';
|
||||||
|
|
||||||
|
if (!interneBelegnummer) {
|
||||||
|
this.logger.log(`Dokument ${doc.id as number} hat keine interne Belegnummer`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let amResults: Awaited<ReturnType<typeof amClient.eingangsrechnungenLivesearch>>;
|
||||||
|
try {
|
||||||
|
amResults = await amClient.eingangsrechnungenLivesearch(interneBelegnummer);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `${interneBelegnummer}: Livesearch-Fehler`;
|
||||||
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
result.errors.push(msg);
|
result.errors.push(msg);
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result.skipped++;
|
if (amResults.length === 0) {
|
||||||
|
this.logger.log(`${interneBelegnummer} nicht in Agrarmonitor gefunden`);
|
||||||
|
result.skipped++;
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amResults.length > 1) {
|
||||||
|
const msg = `${interneBelegnummer}: Mehrfach gefunden`;
|
||||||
|
this.logger.error(msg);
|
||||||
|
result.errors.push(msg);
|
||||||
|
await this.delay(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amDoc = amResults[0];
|
||||||
|
|
||||||
|
if (!amDoc.interneBelegNummer && interneBelegnummer) {
|
||||||
|
try {
|
||||||
|
await amClient.setLieferscheinNummer(amDoc.eingangId, interneBelegnummer);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.logger.warn(`${interneBelegnummer}: Lieferscheinnummer setzen fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amDoc.eingangsDatum) {
|
||||||
|
const eingangsdatumField = ((doc.custom_fields as any[]) ?? []).find(
|
||||||
|
(cf: any) => cf.field === EINGANGSDATUM_FIELD_ID,
|
||||||
|
);
|
||||||
|
if (eingangsdatumField?.value) {
|
||||||
|
const eingangsdatum = new Date(eingangsdatumField.value as string);
|
||||||
|
if (!isNaN(eingangsdatum.getTime())) {
|
||||||
|
await amClient.setEingangsdatum(amDoc.eingangId, eingangsdatum);
|
||||||
|
this.logger.log(`Eingangsdatum für ${interneBelegnummer} gesetzt`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.skipped++;
|
||||||
|
} else if (amDoc.buchungsDatum) {
|
||||||
|
try {
|
||||||
|
let correspondentId: number | undefined;
|
||||||
|
const customer = customers.find((c) => Number(c.id) === amDoc.kundenId);
|
||||||
|
if (customer) {
|
||||||
|
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||||
|
const searchName = `(${lieferantennummer})`;
|
||||||
|
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
||||||
|
let corr = await this.paperlessService.getCorrespondentByName(searchName);
|
||||||
|
if (!corr) {
|
||||||
|
corr = await this.paperlessService.addCorrespondent({
|
||||||
|
name: displayName,
|
||||||
|
match: '',
|
||||||
|
matching_algorithm: 0,
|
||||||
|
is_insensitive: true,
|
||||||
|
owner: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (corr) correspondentId = corr.id as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ownerId: number | undefined;
|
||||||
|
const matchedClient = await this.clientRepo.findOneBy({
|
||||||
|
AgrarmonitorBetriebId: amDoc.betriebId,
|
||||||
|
});
|
||||||
|
if (matchedClient) ownerId = matchedClient.PaperlessUserId;
|
||||||
|
|
||||||
|
const currentTags: number[] = (doc.tags as number[]) ?? [];
|
||||||
|
const newTags = [...new Set(currentTags.filter((t) => t !== tagFertigId).concat([tagVerbuchtId]))];
|
||||||
|
|
||||||
|
const updateData: Record<string, any> = { tags: newTags };
|
||||||
|
if (correspondentId !== undefined) updateData.correspondent = correspondentId;
|
||||||
|
if (ownerId !== undefined) updateData.owner = ownerId;
|
||||||
|
|
||||||
|
await this.paperlessService.updateDocument(doc.id as number, updateData);
|
||||||
|
this.logger.log(`Beleg ${interneBelegnummer} gebucht`);
|
||||||
|
result.updated++;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = `${interneBelegnummer}: Update-Fehler`;
|
||||||
|
this.logger.error(`${msg}: ${err instanceof Error ? err.message : err}`);
|
||||||
|
result.errors.push(msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.skipped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delay(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.delay(500);
|
this.logger.log(
|
||||||
|
`Polling abgeschlossen: ${result.processed} verarbeitet, ${result.updated} aktualisiert, ` +
|
||||||
|
`${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.pollingRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Polling abgeschlossen: ${result.processed} verarbeitet, ${result.updated} aktualisiert, ` +
|
|
||||||
`${result.skipped} übersprungen, ${result.errors.length} Fehler`,
|
|
||||||
);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
// paperless-backend/src/agrarmonitor/agrarmonitor-web.service.ts
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { parse } from 'node-html-parser';
|
|
||||||
import { AgrarmonitorService } from './agrarmonitor.service';
|
|
||||||
|
|
||||||
export interface EingangsrechnungEntry {
|
|
||||||
eingangId: number;
|
|
||||||
belegNummer: string;
|
|
||||||
interneBelegNummer: string;
|
|
||||||
kundenId: number;
|
|
||||||
betriebId: number;
|
|
||||||
buchungsDatum: Date | null;
|
|
||||||
eingangsDatum: Date | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AgrarmonitorWebService {
|
|
||||||
private readonly logger = new Logger(AgrarmonitorWebService.name);
|
|
||||||
private readonly baseUrl: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly agrarmonitorService: AgrarmonitorService,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) {
|
|
||||||
this.baseUrl = this.configService.get<string>(
|
|
||||||
'AGRARMONITOR_BASE_URL',
|
|
||||||
'https://admin7.agrarmonitor.de',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async eingangsrechnungenLivesearch(suchstring: string): Promise<EingangsrechnungEntry[]> {
|
|
||||||
const client = await this.agrarmonitorService.getClient();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.http.get('/');
|
|
||||||
} catch (err: any) {
|
|
||||||
this.logger.warn(`Session-Refresh fehlgeschlagen: ${err?.message}`);
|
|
||||||
}
|
|
||||||
const searchUrl =
|
|
||||||
`/module/dateien/livesearch.php?suchstring=${encodeURIComponent(suchstring)}` +
|
|
||||||
`&stammdatum_typ=-1&mobil=-1&sensibel=-1&firma=0&itemsperpage=100000&seite=1`;
|
|
||||||
const { data: html } = await client.http.get<string>(searchUrl, { responseType: 'text' });
|
|
||||||
|
|
||||||
const root = parse('<doc>' + html + '</doc>');
|
|
||||||
const table = root.querySelector('table#dateien');
|
|
||||||
if (!table) return [];
|
|
||||||
|
|
||||||
const rows = table.querySelectorAll('tbody tr');
|
|
||||||
const results: EingangsrechnungEntry[] = [];
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const tds = row.querySelectorAll('td');
|
|
||||||
if (tds.length < 4) continue;
|
|
||||||
if (!tds[3].text.trim().startsWith('Eingangsrechnungen')) continue;
|
|
||||||
|
|
||||||
const linkEl = tds[3].querySelector('a');
|
|
||||||
if (!linkEl) continue;
|
|
||||||
|
|
||||||
const href = linkEl.getAttribute('href') ?? '';
|
|
||||||
const eingangId = parseInt(href.split('/').pop() ?? '0', 10);
|
|
||||||
if (!eingangId) continue;
|
|
||||||
|
|
||||||
const belegText = linkEl.text;
|
|
||||||
const belegParts = belegText.split(',');
|
|
||||||
const belegNummer = belegParts[0]?.trim() ?? '';
|
|
||||||
|
|
||||||
const { data: editHtml } = await client.http.get<string>(
|
|
||||||
`/module/eingangsrechnungen/api/eingangsrechnungen.php?id=edit&rechnungId=${eingangId}`,
|
|
||||||
{ responseType: 'text' },
|
|
||||||
);
|
|
||||||
const editRoot = parse(editHtml);
|
|
||||||
|
|
||||||
const interneBelegNummer =
|
|
||||||
editRoot.querySelector('input[name="lieferscheinnummer"]')?.getAttribute('value') ?? '';
|
|
||||||
const kundenId = parseInt(
|
|
||||||
editRoot
|
|
||||||
.querySelector('select[name="rgempf"] option[selected]')
|
|
||||||
?.getAttribute('value') ?? '0',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
const betriebId = parseInt(
|
|
||||||
editRoot
|
|
||||||
.querySelector('select[name="firma_id"] option[selected]')
|
|
||||||
?.getAttribute('value') ?? '0',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: detailHtml } = await client.http.get<string>(
|
|
||||||
`/eingangsrechnungen/detail/${eingangId}`,
|
|
||||||
{ responseType: 'text' },
|
|
||||||
);
|
|
||||||
const detailRoot = parse(detailHtml);
|
|
||||||
const receivedEl = detailRoot.getElementById('receivedStatus');
|
|
||||||
|
|
||||||
let eingangsDatum: Date | null = null;
|
|
||||||
let buchungsDatum: Date | null = null;
|
|
||||||
|
|
||||||
if (receivedEl) {
|
|
||||||
const eingangsText = receivedEl.text.trim();
|
|
||||||
const eingangsMatch = eingangsText.match(/Empfangen am (\d{2}\.\d{2}\.\d{2,4})/);
|
|
||||||
if (eingangsMatch) eingangsDatum = this.parseGermanDate(eingangsMatch[1]);
|
|
||||||
|
|
||||||
const parentText = receivedEl.parentNode?.text ?? '';
|
|
||||||
const buchenMatch = parentText.match(/Gebucht am (\d{2}\.\d{2}\.\d{2,4})/);
|
|
||||||
if (buchenMatch) buchungsDatum = this.parseGermanDate(buchenMatch[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ eingangId, belegNummer, interneBelegNummer, kundenId, betriebId, buchungsDatum, eingangsDatum });
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEingangsdatum(eingangId: number, datum: Date): Promise<boolean> {
|
|
||||||
const client = await this.agrarmonitorService.getClient();
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('datum', this.formatGermanDate(datum));
|
|
||||||
params.append('receiptID', String(eingangId));
|
|
||||||
try {
|
|
||||||
const res = await client.http.post(
|
|
||||||
'/module/eingangsrechnungen/api/updateReceived.php',
|
|
||||||
params.toString(),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Referer: `${this.baseUrl}/eingangsrechnungen/detail/${eingangId}`,
|
|
||||||
Origin: this.baseUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return res.status < 400;
|
|
||||||
} catch (err: any) {
|
|
||||||
this.logger.error(`setEingangsdatum(${eingangId}) Fehler: ${err?.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setLieferscheinNummer(eingangId: number, lieferscheinNummer: string): Promise<boolean> {
|
|
||||||
const client = await this.agrarmonitorService.getClient();
|
|
||||||
try {
|
|
||||||
const { data: editHtml } = await client.http.get<string>(
|
|
||||||
`/module/eingangsrechnungen/api/eingangsrechnungen.php?id=edit&rechnungId=${eingangId}`,
|
|
||||||
{ responseType: 'text' },
|
|
||||||
);
|
|
||||||
const editRoot = parse(editHtml);
|
|
||||||
|
|
||||||
const rechnungsnummer =
|
|
||||||
editRoot.querySelector('input[name="rechnungsnummer"]')?.getAttribute('value') ?? '';
|
|
||||||
const rechnungsdatum =
|
|
||||||
editRoot.querySelector('input[name="rechnungsdatum"]')?.getAttribute('value') ?? '';
|
|
||||||
const rgempf =
|
|
||||||
editRoot
|
|
||||||
.querySelector('select[name="rgempf"] option[selected]')
|
|
||||||
?.getAttribute('value') ?? '';
|
|
||||||
const addressName =
|
|
||||||
editRoot.querySelector('input[name="addressName"]')?.getAttribute('value') ?? '';
|
|
||||||
|
|
||||||
if (!rgempf) {
|
|
||||||
this.logger.warn(`setLieferscheinNummer(${eingangId}): kein Empfänger im Formular, übersprungen`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('lieferscheinnummer', lieferscheinNummer);
|
|
||||||
params.append('rechnungsnummer', rechnungsnummer);
|
|
||||||
params.append('rechnungsdatum', rechnungsdatum);
|
|
||||||
params.append('rgempf', rgempf);
|
|
||||||
params.append('adresstext', addressName);
|
|
||||||
|
|
||||||
const res = await client.http.post(
|
|
||||||
`/module/eingangsrechnungen/api/eingangsrechnungen.php?id=update&rechnungId=${eingangId}`,
|
|
||||||
params.toString(),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
Referer: `${this.baseUrl}/eingangsrechnungen/detail/${eingangId}`,
|
|
||||||
Origin: this.baseUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return res.status < 400;
|
|
||||||
} catch (err: any) {
|
|
||||||
this.logger.error(`setLieferscheinNummer(${eingangId}) Fehler: ${err?.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseGermanDate(str: string): Date | null {
|
|
||||||
const parts = str.trim().split('.');
|
|
||||||
if (parts.length !== 3) return null;
|
|
||||||
const [dd, mm, yy] = parts;
|
|
||||||
const yearRaw = parseInt(yy, 10);
|
|
||||||
const year = yy.length === 2
|
|
||||||
? (yearRaw < 50 ? 2000 + yearRaw : 1900 + yearRaw)
|
|
||||||
: yearRaw;
|
|
||||||
const d = new Date(year, parseInt(mm, 10) - 1, parseInt(dd, 10));
|
|
||||||
return isNaN(d.getTime()) ? null : d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatGermanDate(date: Date): string {
|
|
||||||
const dd = String(date.getDate()).padStart(2, '0');
|
|
||||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const yy = String(date.getFullYear()).slice(-2);
|
|
||||||
return `${dd}.${mm}.${yy}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AgrarmonitorService } from './agrarmonitor.service';
|
import { AgrarmonitorService } from './agrarmonitor.service';
|
||||||
|
import { AgrarmonitorPollingService } from './agrarmonitor-polling.service';
|
||||||
import { AgrarmonitorController } from './agrarmonitor.controller';
|
import { AgrarmonitorController } from './agrarmonitor.controller';
|
||||||
|
import { PaperlessModule } from '../paperless/paperless.module';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
import { Client } from '../database/entities/client.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [AgrarmonitorService],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Setting, Client]),
|
||||||
|
PaperlessModule,
|
||||||
|
],
|
||||||
|
providers: [AgrarmonitorService, AgrarmonitorPollingService],
|
||||||
controllers: [AgrarmonitorController],
|
controllers: [AgrarmonitorController],
|
||||||
exports: [AgrarmonitorService],
|
exports: [AgrarmonitorService],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user