feat: add Agrarmonitor correspondent sync
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 39s
- Extract getOrCreateCorrespondent helper to deduplicate logic - Add syncCorrespondentIds to match Paperless correspondents to Agrarmonitor IDs via Lieferantennummer and persist in CorrespondentSetting - New POST /api/agrarmonitor/sync-correspondents endpoint - "Agrarmonitor-Abgleich" button in Correspondents settings tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { AgrarmonitorService } from './agrarmonitor.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';
|
||||||
|
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||||
|
|
||||||
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
||||||
const EINGANGSDATUM_FIELD_ID = 9;
|
const EINGANGSDATUM_FIELD_ID = 9;
|
||||||
@@ -31,6 +32,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
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>,
|
||||||
|
@InjectRepository(CorrespondentSetting) private readonly corrSettingRepo: Repository<CorrespondentSetting>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
@@ -128,18 +130,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
||||||
)) {
|
)) {
|
||||||
try {
|
try {
|
||||||
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
await this.getOrCreateCorrespondent(customer);
|
||||||
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
|
||||||
const existing = await this.paperlessService.getCorrespondentByName(displayName);
|
|
||||||
if (!existing) {
|
|
||||||
await this.paperlessService.addCorrespondent({
|
|
||||||
name: displayName,
|
|
||||||
match: '',
|
|
||||||
matching_algorithm: 0,
|
|
||||||
is_insensitive: true,
|
|
||||||
owner: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.logger.warn(`Korrespondenten-Sync fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
|
this.logger.warn(`Korrespondenten-Sync fehlgeschlagen: ${err instanceof Error ? err.message : err}`);
|
||||||
}
|
}
|
||||||
@@ -233,18 +224,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
let correspondentId: number | undefined;
|
let correspondentId: number | undefined;
|
||||||
const customer = customers.find((c) => Number(c.id) === amDoc.kundenId);
|
const customer = customers.find((c) => Number(c.id) === amDoc.kundenId);
|
||||||
if (customer) {
|
if (customer) {
|
||||||
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
const corr = await this.getOrCreateCorrespondent(customer);
|
||||||
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
|
||||||
let corr = await this.paperlessService.getCorrespondentByName(displayName);
|
|
||||||
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;
|
if (corr) correspondentId = corr.id as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,17 +396,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Korrespondent ermitteln oder anlegen
|
// Korrespondent ermitteln oder anlegen
|
||||||
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
const corr = await this.getOrCreateCorrespondent(customer);
|
||||||
let corr = await this.paperlessService.getCorrespondentByName(displayName);
|
|
||||||
if (!corr) {
|
|
||||||
corr = await this.paperlessService.addCorrespondent({
|
|
||||||
name: displayName,
|
|
||||||
match: '',
|
|
||||||
matching_algorithm: 0,
|
|
||||||
is_insensitive: true,
|
|
||||||
owner: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Owner aus Client-Tabelle
|
// Owner aus Client-Tabelle
|
||||||
let ownerId: number | undefined;
|
let ownerId: number | undefined;
|
||||||
@@ -494,6 +464,71 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncCorrespondentIds(): Promise<{ total: number; matched: number; unmatched: number }> {
|
||||||
|
let amClient: Awaited<ReturnType<typeof this.agrarmonitorService.getClient>>;
|
||||||
|
try {
|
||||||
|
amClient = await this.agrarmonitorService.getClient();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw new Error(`Connector-Fehler: ${err instanceof Error ? err.message : 'unbekannt'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customers = await amClient.fetchCustomers();
|
||||||
|
const lieferantMap = new Map<string, number>();
|
||||||
|
for (const c of customers) {
|
||||||
|
const nr = String(c['lieferantennummer'] ?? '').trim();
|
||||||
|
if (nr) lieferantMap.set(nr, Number(c.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allCorrespondents: any[] = [];
|
||||||
|
let page = 1;
|
||||||
|
while (true) {
|
||||||
|
const resp = await this.paperlessService.getCorrespondents({ page, page_size: 250 });
|
||||||
|
allCorrespondents.push(...(resp.results ?? []));
|
||||||
|
if (!resp.next) break;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lieferantRegex = /\((\d+)\)$/;
|
||||||
|
let matched = 0;
|
||||||
|
let unmatched = 0;
|
||||||
|
|
||||||
|
for (const corr of allCorrespondents) {
|
||||||
|
const m = lieferantRegex.exec(corr.name as string);
|
||||||
|
if (!m) { unmatched++; continue; }
|
||||||
|
|
||||||
|
const amId = lieferantMap.get(m[1]);
|
||||||
|
if (amId === undefined) { unmatched++; continue; }
|
||||||
|
|
||||||
|
let setting = await this.corrSettingRepo.findOneBy({ CorrespondentId: corr.id as number });
|
||||||
|
if (!setting) {
|
||||||
|
setting = this.corrSettingRepo.create({ CorrespondentId: corr.id as number, AgrarmonitorId: amId });
|
||||||
|
} else {
|
||||||
|
setting.AgrarmonitorId = amId;
|
||||||
|
}
|
||||||
|
await this.corrSettingRepo.save(setting);
|
||||||
|
matched++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Korrespondenten-Abgleich: ${matched} zugeordnet, ${unmatched} ohne Treffer`);
|
||||||
|
return { total: allCorrespondents.length, matched, unmatched };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateCorrespondent(customer: Record<string, unknown>): Promise<any> {
|
||||||
|
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||||
|
const displayName = this.buildCustomerName(customer, lieferantennummer);
|
||||||
|
let corr = await this.paperlessService.getCorrespondentByName(displayName);
|
||||||
|
if (!corr) {
|
||||||
|
corr = await this.paperlessService.addCorrespondent({
|
||||||
|
name: displayName,
|
||||||
|
match: '',
|
||||||
|
matching_algorithm: 0,
|
||||||
|
is_insensitive: true,
|
||||||
|
owner: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return corr;
|
||||||
|
}
|
||||||
|
|
||||||
private buildCustomerName(customer: Record<string, unknown>, nummer: string): string {
|
private buildCustomerName(customer: Record<string, unknown>, nummer: string): string {
|
||||||
const firma = (customer['firma'] as string) ?? '';
|
const firma = (customer['firma'] as string) ?? '';
|
||||||
const nachname = (customer['nachname'] as string) ?? '';
|
const nachname = (customer['nachname'] as string) ?? '';
|
||||||
|
|||||||
@@ -49,4 +49,11 @@ export class AgrarmonitorController {
|
|||||||
async processUploads() {
|
async processUploads() {
|
||||||
return this.pollingService.processVerarbeiteteDocuments();
|
return this.pollingService.processVerarbeiteteDocuments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('sync-correspondents')
|
||||||
|
@HttpCode(200)
|
||||||
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
||||||
|
async syncCorrespondents() {
|
||||||
|
return this.pollingService.syncCorrespondentIds();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { AgrarmonitorController } from './agrarmonitor.controller';
|
|||||||
import { PaperlessModule } from '../paperless/paperless.module';
|
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';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Setting, Client]),
|
TypeOrmModule.forFeature([Setting, Client, CorrespondentSetting]),
|
||||||
PaperlessModule,
|
PaperlessModule,
|
||||||
],
|
],
|
||||||
providers: [AgrarmonitorService, AgrarmonitorPollingService],
|
providers: [AgrarmonitorService, AgrarmonitorPollingService],
|
||||||
|
|||||||
@@ -225,4 +225,6 @@ export const agrarmonitorApi = {
|
|||||||
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/run-polling').then((r) => r.data),
|
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/run-polling').then((r) => r.data),
|
||||||
processUploads: () =>
|
processUploads: () =>
|
||||||
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/process-uploads').then((r) => r.data),
|
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/process-uploads').then((r) => r.data),
|
||||||
|
syncCorrespondents: () =>
|
||||||
|
api.post<{ total: number; matched: number; unmatched: number }>('/api/agrarmonitor/sync-correspondents').then((r) => r.data),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1408,6 +1408,7 @@ function CorrespondentsTab() {
|
|||||||
const [pageSize, setPageSize] = useState(50);
|
const [pageSize, setPageSize] = useState(50);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
const [syncLoading, setSyncLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const load = useCallback(async (page: number, size: number, search?: string) => {
|
const load = useCallback(async (page: number, size: number, search?: string) => {
|
||||||
@@ -1440,6 +1441,19 @@ function CorrespondentsTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSync = async () => {
|
||||||
|
setSyncLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await agrarmonitorApi.syncCorrespondents();
|
||||||
|
message.success(`Abgleich abgeschlossen: ${result.matched} zugeordnet, ${result.unmatched} ohne Treffer (${result.total} gesamt)`);
|
||||||
|
load(currentPage, pageSize, searchText);
|
||||||
|
} catch (err) {
|
||||||
|
message.error('Fehler beim Agrarmonitor-Abgleich');
|
||||||
|
} finally {
|
||||||
|
setSyncLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateAgrarmonitorId = async (id: number, val: number | null) => {
|
const updateAgrarmonitorId = async (id: number, val: number | null) => {
|
||||||
try {
|
try {
|
||||||
await settingsApi.updateCorrespondentSetting(id, val);
|
await settingsApi.updateCorrespondentSetting(id, val);
|
||||||
@@ -1497,9 +1511,14 @@ function CorrespondentsTab() {
|
|||||||
onSearch={(v) => { setSearchText(v); setCurrentPage(1); }}
|
onSearch={(v) => { setSearchText(v); setCurrentPage(1); }}
|
||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
/>
|
/>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalOpen(true)}>
|
<Space>
|
||||||
Neuen Korrespondenten anlegen
|
<Button icon={<GlobalOutlined />} loading={syncLoading} onClick={handleSync}>
|
||||||
</Button>
|
Agrarmonitor-Abgleich
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalOpen(true)}>
|
||||||
|
Neuen Korrespondenten anlegen
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user