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 { Setting } from '../database/entities/setting.entity';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
|
||||
const INTERN_BELEGNUMMER_FIELD_ID = 7;
|
||||
const EINGANGSDATUM_FIELD_ID = 9;
|
||||
@@ -31,6 +32,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
private readonly paperlessService: PaperlessService,
|
||||
@InjectRepository(Setting) private readonly settingRepo: Repository<Setting>,
|
||||
@InjectRepository(Client) private readonly clientRepo: Repository<Client>,
|
||||
@InjectRepository(CorrespondentSetting) private readonly corrSettingRepo: Repository<CorrespondentSetting>,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
@@ -128,18 +130,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
(c) => Number(c['ist_lieferant']) === 1 && Number(c['ist_aktiv']) === 1,
|
||||
)) {
|
||||
try {
|
||||
const lieferantennummer = (customer['lieferantennummer'] as string) ?? '';
|
||||
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,
|
||||
});
|
||||
}
|
||||
await this.getOrCreateCorrespondent(customer);
|
||||
} catch (err: unknown) {
|
||||
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;
|
||||
const customer = customers.find((c) => Number(c.id) === amDoc.kundenId);
|
||||
if (customer) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
const corr = await this.getOrCreateCorrespondent(customer);
|
||||
if (corr) correspondentId = corr.id as number;
|
||||
}
|
||||
|
||||
@@ -416,17 +396,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
// Korrespondent ermitteln oder anlegen
|
||||
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,
|
||||
});
|
||||
}
|
||||
const corr = await this.getOrCreateCorrespondent(customer);
|
||||
|
||||
// Owner aus Client-Tabelle
|
||||
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 {
|
||||
const firma = (customer['firma'] as string) ?? '';
|
||||
const nachname = (customer['nachname'] as string) ?? '';
|
||||
|
||||
@@ -49,4 +49,11 @@ export class AgrarmonitorController {
|
||||
async processUploads() {
|
||||
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 { Setting } from '../database/entities/setting.entity';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Setting, Client]),
|
||||
TypeOrmModule.forFeature([Setting, Client, CorrespondentSetting]),
|
||||
PaperlessModule,
|
||||
],
|
||||
providers: [AgrarmonitorService, AgrarmonitorPollingService],
|
||||
|
||||
@@ -225,4 +225,6 @@ export const agrarmonitorApi = {
|
||||
api.post<AgrarmonitorPollingResult>('/api/agrarmonitor/run-polling').then((r) => r.data),
|
||||
processUploads: () =>
|
||||
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 [searchText, setSearchText] = useState('');
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [syncLoading, setSyncLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
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) => {
|
||||
try {
|
||||
await settingsApi.updateCorrespondentSetting(id, val);
|
||||
@@ -1497,9 +1511,14 @@ function CorrespondentsTab() {
|
||||
onSearch={(v) => { setSearchText(v); setCurrentPage(1); }}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalOpen(true)}>
|
||||
Neuen Korrespondenten anlegen
|
||||
</Button>
|
||||
<Space>
|
||||
<Button icon={<GlobalOutlined />} loading={syncLoading} onClick={handleSync}>
|
||||
Agrarmonitor-Abgleich
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalOpen(true)}>
|
||||
Neuen Korrespondenten anlegen
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user