fix: Berechtigungen der Tagesübersicht, WebDAV-Ausbau, Tests instand gesetzt
Build and Push Multi-Platform Images / build-and-push (push) Successful in 55s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 55s
Die tägliche E-Mail-Zusammenfassung zeigte Nutzern Bereiche, für die sie
keine Berechtigung haben. Zwei unabhängige Ursachen:
- Das Backend prüfte weiterhin die Altgruppe PM_Belege. Die Umbenennung
zu PM_Buchhaltung (ca1d371) war nur im Frontend angekommen, weshalb
der Digest "Manuell bearbeiten" und "In Agrarmonitor" anbot, während
die Oberfläche beide Bereiche sperrte.
- Der Cron-Versand wertet die Gruppen aus user_settings aus. Diese Spalte
wurde nur beim Aufruf der Benutzereinstellungen gefüllt; entzogene
Berechtigungen erreichten den Digest daher unter Umständen nie.
Behoben durch Angleichen des Gruppen-Mappings und den neuen
UserIdentitySyncService, der E-Mail, Benutzername und Gruppen bei jedem
authentifizierten Request aus dem Token spiegelt – ohne den Request zu
blockieren und ohne DB-Zugriff, solange sich das Token nicht ändert. Die
doppelte Identitätspflege im UserSettingsService entfällt.
WebDAV wird nicht eingesetzt und ist entfernt; Export-Ziele bieten nur
noch FTP. Damit verschwindet das ESM-Paket webdav, an dem zwei
Jest-Suites bereits beim Parsen scheiterten.
Veraltete Tests instand gesetzt: email.controller und settings.controller
mockten weniger Abhängigkeiten, als die Klassen inzwischen haben;
postprocessing.service.spec beschrieb noch das alte Regelmodell mit
Einzelfeldern statt FilterJson und ist gegen die heutige Filter-Engine
neu geschrieben (AND/OR, verschachtelte Gruppen, Fehlerprotokollierung).
Enthält außerdem den Arbeitsstand der E-Rechnungs-Mandantenzuordnung, da
sich beide Änderungen dieselben Dateien teilen (SettingsPage, package.json).
98 Tests in 12 Suites grün, Backend- und Frontend-Build sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,23 @@ export interface DocumentRequirement {
|
||||
fieldOptions?: { id: string | number; label: string }[];
|
||||
}
|
||||
|
||||
export interface ERechnungVorschlag {
|
||||
/** Ob das Dokument überhaupt eine auswertbare E-Rechnung enthält. */
|
||||
erechnung: boolean;
|
||||
mandantId: number | null;
|
||||
belegartId: number | null;
|
||||
/** Kurztext, wenn kein Vorschlag möglich war. */
|
||||
hinweis: string | null;
|
||||
rechnung: {
|
||||
rechnungsnummer: string | null;
|
||||
rechnungsdatum: string | null;
|
||||
gesamtbetrag: number | null;
|
||||
waehrung: string | null;
|
||||
verkaeufer: string | null;
|
||||
typeCode: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface Kontonummer {
|
||||
KontonummerId?: number;
|
||||
CorrespondentId: number;
|
||||
@@ -54,4 +71,7 @@ export const posteingangApi = {
|
||||
|
||||
createKontonummer: (data: { correspondentId: number; nummer: string }) =>
|
||||
api.post<Kontonummer>('/api/kontonummern', data).then(r => r.data),
|
||||
|
||||
getErechnung: (id: number) =>
|
||||
api.get<ERechnungVorschlag>(`/api/paperless/inbox/${id}/erechnung`).then(r => r.data),
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface SettingDocType {
|
||||
TagNotReady: number | null;
|
||||
TagReady: number | null;
|
||||
FreigabeErforderlich?: boolean | null;
|
||||
/** Rechnungstyp-Codes (BT-3) einer E-Rechnung, kommagetrennt (z. B. "380,384") */
|
||||
ERechnungTypeCodes?: string | null;
|
||||
}
|
||||
|
||||
export interface SettingDocField {
|
||||
@@ -53,7 +55,7 @@ export interface SettingPostprocessingAction {
|
||||
export interface SettingExportTarget {
|
||||
Id: number;
|
||||
Name: string;
|
||||
Protocol: string; // 'ftp' | 'webdav'
|
||||
Protocol: string; // 'ftp'
|
||||
Host: string;
|
||||
Port: number | null;
|
||||
Username: string | null;
|
||||
@@ -86,6 +88,22 @@ export interface SettingClient {
|
||||
AgrarmonitorBetriebId: number | null;
|
||||
}
|
||||
|
||||
/** Kennungsarten, über die ein Mandant in einer E-Rechnung erkannt wird. */
|
||||
export type ClientIdentifierTyp = 'ustid' | 'leitwegid' | 'kaeuferkennung';
|
||||
|
||||
export interface SettingClientIdentifier {
|
||||
Id: number;
|
||||
ClientId: number;
|
||||
Typ: ClientIdentifierTyp;
|
||||
Wert: string;
|
||||
}
|
||||
|
||||
export const CLIENT_IDENTIFIER_LABELS: Record<ClientIdentifierTyp, string> = {
|
||||
ustid: 'USt-IdNr.',
|
||||
leitwegid: 'Leitweg-ID',
|
||||
kaeuferkennung: 'Käufer-Kennung',
|
||||
};
|
||||
|
||||
export const settingsApi = {
|
||||
// Dokumenttypen
|
||||
getDocTypes: () => api.get<SettingDocType[]>('/api/settings/document-types').then(r => r.data),
|
||||
@@ -162,6 +180,14 @@ export const settingsApi = {
|
||||
updateClient: (id: number, AgrarmonitorBetriebId: number | null) =>
|
||||
api.put<SettingClient>(`/api/settings/clients/${id}`, { AgrarmonitorBetriebId }).then(r => r.data),
|
||||
|
||||
// E-Rechnungs-Kennungen der Mandanten
|
||||
getClientIdentifiers: () =>
|
||||
api.get<SettingClientIdentifier[]>('/api/settings/client-identifiers').then(r => r.data),
|
||||
createClientIdentifier: (data: { ClientId: number; Typ: ClientIdentifierTyp; Wert: string }) =>
|
||||
api.post<SettingClientIdentifier>('/api/settings/client-identifiers', data).then(r => r.data),
|
||||
deleteClientIdentifier: (id: number) =>
|
||||
api.delete(`/api/settings/client-identifiers/${id}`).then(r => r.data),
|
||||
|
||||
// Inbox-Postprozessor (global, deprecated)
|
||||
listInboxActions: () =>
|
||||
api.get<InboxAction[]>('/api/settings/inbox-actions').then((r) => r.data),
|
||||
@@ -191,7 +217,7 @@ export interface InboxAction {
|
||||
|
||||
export const INBOX_ACTION_LABELS: Record<InboxActionType, string> = {
|
||||
MAIL: 'Per E-Mail senden',
|
||||
EXPORT: 'Export (FTP/WebDAV)',
|
||||
EXPORT: 'Export (FTP)',
|
||||
PAPERLESS: 'In Paperless importieren',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Modal, Form, Select, DatePicker, Input, Spin, message, Row, Col, Button, Space, Divider, Tag } from 'antd';
|
||||
import { Modal, Form, Select, DatePicker, Input, Spin, message, Row, Col, Button, Space, Divider, Tag, Alert } from 'antd';
|
||||
import { PlusOutlined, EyeOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { posteingangApi } from '../api/posteingang';
|
||||
import type { DocumentRequirement, PosteingangDocument, Kontonummer } from '../api/posteingang';
|
||||
import type { DocumentRequirement, PosteingangDocument, Kontonummer, ERechnungVorschlag } from '../api/posteingang';
|
||||
import { clientsApi } from '../api/inbox';
|
||||
import type { Client } from '../api/inbox';
|
||||
import { paperlessApi } from '../api/paperless';
|
||||
@@ -42,6 +42,10 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
const [steuertagIds, setSteuertagIds] = useState<number[]>([]);
|
||||
const contentTags = allTags.filter(t => !steuertagIds.includes(t.id));
|
||||
|
||||
// E-Rechnung: ausgelesener Vorschlag und die Felder, die daraus befüllt wurden
|
||||
const [erechnung, setErechnung] = useState<ERechnungVorschlag | null>(null);
|
||||
const [ausERechnung, setAusERechnung] = useState<{ mandant: boolean, documentType: boolean }>({ mandant: false, documentType: false });
|
||||
|
||||
const [kontonummerMissing, setKontonummerMissing] = useState<{ correspondentId: number, nummer: string } | null>(null);
|
||||
const [docTitles, setDocTitles] = useState<Record<number, string>>({});
|
||||
const [searchModalOpen, setSearchModalOpen] = useState<{ field: string, reqId: number } | null>(null);
|
||||
@@ -117,6 +121,8 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
setKontonummerMissing(null);
|
||||
setKontonummern([]);
|
||||
setNewKontonummer('');
|
||||
setErechnung(null);
|
||||
setAusERechnung({ mandant: false, documentType: false });
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
@@ -155,6 +161,41 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
}
|
||||
}, [document, open, steuertagIds, form]);
|
||||
|
||||
// E-Rechnung auswerten und Mandant/Belegart vorschlagen. Es werden nur leere
|
||||
// Felder befüllt – ein bereits gesetzter Wert stammt vom Benutzer oder einer
|
||||
// Regel und wird nicht überschrieben.
|
||||
useEffect(() => {
|
||||
setErechnung(null);
|
||||
setAusERechnung({ mandant: false, documentType: false });
|
||||
if (!open || !isPosteingang || !documentId) return;
|
||||
|
||||
let abgebrochen = false;
|
||||
(async () => {
|
||||
try {
|
||||
const vorschlag = await posteingangApi.getErechnung(documentId);
|
||||
if (abgebrochen || !vorschlag.erechnung) return;
|
||||
setErechnung(vorschlag);
|
||||
|
||||
const uebernommen = { mandant: false, documentType: false };
|
||||
if (vorschlag.mandantId && !form.getFieldValue('mandant')) {
|
||||
form.setFieldValue('mandant', vorschlag.mandantId);
|
||||
uebernommen.mandant = true;
|
||||
}
|
||||
if (vorschlag.belegartId && !form.getFieldValue('documentType')) {
|
||||
form.setFieldValue('documentType', vorschlag.belegartId);
|
||||
uebernommen.documentType = true;
|
||||
fetchRequirements(vorschlag.belegartId);
|
||||
}
|
||||
setAusERechnung(uebernommen);
|
||||
} catch {
|
||||
// Die Auswertung ist eine Hilfestellung – ein Fehler darf das
|
||||
// Bearbeiten nicht stören und wird deshalb nicht gemeldet.
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { abgebrochen = true; };
|
||||
}, [open, isPosteingang, documentId]);
|
||||
|
||||
const ensureCorrespondentInList = async (correspondentId: number | null | undefined) => {
|
||||
if (!correspondentId) return;
|
||||
|
||||
@@ -252,9 +293,33 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
};
|
||||
|
||||
const handleDocumentTypeChange = (value: number) => {
|
||||
setAusERechnung(prev => ({ ...prev, documentType: false }));
|
||||
fetchRequirements(value);
|
||||
};
|
||||
|
||||
/** Kurzbeschreibung der erkannten Rechnung zur Sichtkontrolle. */
|
||||
const beschreibeRechnung = (v: ERechnungVorschlag) => {
|
||||
const r = v.rechnung;
|
||||
const teile: string[] = [];
|
||||
if (r?.verkaeufer) teile.push(r.verkaeufer);
|
||||
if (r?.rechnungsnummer) teile.push(`Nr. ${r.rechnungsnummer}`);
|
||||
if (r?.rechnungsdatum) teile.push(dayjs(r.rechnungsdatum).format('DD.MM.YYYY'));
|
||||
if (r?.gesamtbetrag !== null && r?.gesamtbetrag !== undefined) {
|
||||
const waehrung = /^[A-Z]{3}$/.test(r.waehrung || '') ? r.waehrung! : 'EUR';
|
||||
teile.push(new Intl.NumberFormat('de-DE', { style: 'currency', currency: waehrung }).format(r.gesamtbetrag));
|
||||
}
|
||||
return teile.length ? teile.join(' \u00b7 ') : 'Keine weiteren Angaben im Rechnungs-XML.';
|
||||
};
|
||||
|
||||
/** Label mit Chip, solange der Wert aus der E-Rechnung stammt. */
|
||||
const labelMitHerkunft = (text: string, ausRechnung: boolean) =>
|
||||
ausRechnung ? (
|
||||
<Space size={4}>
|
||||
{text}
|
||||
<Tag color="blue" style={{ marginInlineEnd: 0 }}>aus E-Rechnung</Tag>
|
||||
</Space>
|
||||
) : text;
|
||||
|
||||
const handleSaveDocument = async (values: any, isNext: boolean = false) => {
|
||||
// Collect custom fields into an array
|
||||
const customFieldsObj: any = {};
|
||||
@@ -279,6 +344,9 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
date: values.belegdatum ? values.belegdatum.format('YYYY-MM-DD') : null,
|
||||
tags: values.tags || [],
|
||||
customFields: customFieldsObj,
|
||||
// Signal fürs Backend, die Käufer-Kennungen auf den gespeicherten
|
||||
// Mandanten fortzuschreiben.
|
||||
erechnung: erechnung?.erechnung === true,
|
||||
};
|
||||
|
||||
setSaving(true);
|
||||
@@ -394,17 +462,37 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
lg={10}
|
||||
style={isMobile ? {} : { overflowY: 'auto', paddingRight: '1rem', borderRight: '1px solid #f0f0f0' }}
|
||||
>
|
||||
{erechnung?.erechnung && (
|
||||
<Alert
|
||||
type={erechnung.hinweis ? 'warning' : 'info'}
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="E-Rechnung erkannt"
|
||||
description={
|
||||
<>
|
||||
<div>{beschreibeRechnung(erechnung)}</div>
|
||||
{erechnung.hinweis && <div style={{ marginTop: 4 }}>{erechnung.hinweis}</div>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form form={form} layout="vertical" disabled={saving}>
|
||||
|
||||
<Form.Item name="mandant" label="Mandant" rules={[{ required: true, message: 'Wähle einen Mandanten' }]}>
|
||||
<Select showSearch optionFilterProp="children" allowClear>
|
||||
<Form.Item name="mandant" label={labelMitHerkunft('Mandant', ausERechnung.mandant)} rules={[{ required: true, message: 'Wähle einen Mandanten' }]}>
|
||||
<Select
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
allowClear
|
||||
onChange={() => setAusERechnung(prev => ({ ...prev, mandant: false }))}
|
||||
>
|
||||
{clients.map(c => (
|
||||
<Option key={c.Id} value={c.PaperlessUserId}>{c.Name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="documentType" label="Dokumentart" rules={[{ required: true, message: 'Wähle eine Dokumentart' }]}>
|
||||
<Form.Item name="documentType" label={labelMitHerkunft('Dokumentart', ausERechnung.documentType)} rules={[{ required: true, message: 'Wähle eine Dokumentart' }]}>
|
||||
<Select showSearch optionFilterProp="children" onChange={handleDocumentTypeChange}>
|
||||
{documentTypes.map(d => (
|
||||
<Option key={d.id} value={d.id}>{d.name}</Option>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
agrarmonitorApi, type AgrarmonitorStatusData,
|
||||
type SettingClient, type AgrarmonitorPollingConfig, type AgrarmonitorPollingResult,
|
||||
type SyncConflict,
|
||||
type SettingClientIdentifier, type ClientIdentifierTyp, CLIENT_IDENTIFIER_LABELS,
|
||||
} from '../api/settings';
|
||||
import { clientsApi, type Client } from '../api/inbox';
|
||||
import { apiKeysApi, type ApiKey } from '../api/api-keys';
|
||||
@@ -223,6 +224,12 @@ function UserClientsTab() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// E-Rechnungs-Kennungen der Mandanten
|
||||
const [identifiers, setIdentifiers] = useState<SettingClientIdentifier[]>([]);
|
||||
const [identLoading, setIdentLoading] = useState(false);
|
||||
const [identModalOpen, setIdentModalOpen] = useState(false);
|
||||
const [identForm] = Form.useForm();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -235,6 +242,13 @@ function UserClientsTab() {
|
||||
} finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
const loadIdentifiers = useCallback(async () => {
|
||||
setIdentLoading(true);
|
||||
try {
|
||||
setIdentifiers(await settingsApi.getClientIdentifiers());
|
||||
} finally { setIdentLoading(false); }
|
||||
}, []);
|
||||
|
||||
const loadAllClients = useCallback(async () => {
|
||||
setClientsLoading(true);
|
||||
try {
|
||||
@@ -243,7 +257,7 @@ function UserClientsTab() {
|
||||
} finally { setClientsLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); loadAllClients(); }, [load, loadAllClients]);
|
||||
useEffect(() => { load(); loadAllClients(); loadIdentifiers(); }, [load, loadAllClients, loadIdentifiers]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const values = await form.validateFields();
|
||||
@@ -269,6 +283,52 @@ function UserClientsTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddIdentifier = async () => {
|
||||
const values = await identForm.validateFields();
|
||||
try {
|
||||
await settingsApi.createClientIdentifier(values);
|
||||
message.success('Kennung hinzugefügt');
|
||||
setIdentModalOpen(false);
|
||||
identForm.resetFields();
|
||||
loadIdentifiers();
|
||||
} catch (e) {
|
||||
const meldung = (e as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
message.error(meldung ?? 'Kennung konnte nicht angelegt werden');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteIdentifier = async (id: number) => {
|
||||
await settingsApi.deleteClientIdentifier(id);
|
||||
message.success('Gelöscht');
|
||||
loadIdentifiers();
|
||||
};
|
||||
|
||||
const identifierColumns: ColumnsType<SettingClientIdentifier> = [
|
||||
{
|
||||
title: 'Betrieb',
|
||||
key: 'client',
|
||||
render: (_, r) => allClients.find(c => c.Id === r.ClientId)?.Name ?? r.ClientId,
|
||||
},
|
||||
{
|
||||
title: 'Art',
|
||||
dataIndex: 'Typ',
|
||||
key: 'typ',
|
||||
width: 160,
|
||||
render: (t: ClientIdentifierTyp) => <Tag>{CLIENT_IDENTIFIER_LABELS[t] ?? t}</Tag>,
|
||||
},
|
||||
{ title: 'Kennung', dataIndex: 'Wert', key: 'wert' },
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 80,
|
||||
render: (_, record) => (
|
||||
<Popconfirm title="Kennung wirklich löschen?" onConfirm={() => handleDeleteIdentifier(record.Id)}>
|
||||
<Button danger icon={<DeleteOutlined />} size="small" />
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const allClientColumns: ColumnsType<SettingClient> = [
|
||||
{ title: 'Name', dataIndex: 'Name', key: 'name' },
|
||||
{
|
||||
@@ -338,6 +398,62 @@ function UserClientsTab() {
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<Typography.Title level={5} style={{ marginBottom: 8 }}>Betriebe — E-Rechnungs-Kennungen</Typography.Title>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||
Über diese Käufer-Kennungen wird der Mandant aus einer E-Rechnung erkannt. Neue Kennungen
|
||||
werden beim Speichern eines Belegs automatisch gelernt — hier lassen sie sich prüfen,
|
||||
ergänzen und korrigieren. Jede Kennung darf nur einem Betrieb gehören.
|
||||
</Typography.Text>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIdentModalOpen(true)}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
Kennung hinzufügen
|
||||
</Button>
|
||||
<Table
|
||||
dataSource={identifiers}
|
||||
columns={identifierColumns}
|
||||
loading={identLoading}
|
||||
rowKey="Id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
locale={{ emptyText: 'Noch keine Kennungen — die erste entsteht, sobald ein Beleg mit E-Rechnung gespeichert wird.' }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="Neue E-Rechnungs-Kennung"
|
||||
open={identModalOpen}
|
||||
onOk={handleAddIdentifier}
|
||||
onCancel={() => { setIdentModalOpen(false); identForm.resetFields(); }}
|
||||
>
|
||||
<Form form={identForm} layout="vertical">
|
||||
<Form.Item name="ClientId" label="Betrieb" rules={[{ required: true, message: 'Betrieb wählen' }]}>
|
||||
<Select showSearch optionFilterProp="children">
|
||||
{allClients.map(c => <Select.Option key={c.Id} value={c.Id}>{c.Name}</Select.Option>)}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="Typ" label="Art der Kennung" rules={[{ required: true, message: 'Art wählen' }]}>
|
||||
<Select>
|
||||
{(Object.keys(CLIENT_IDENTIFIER_LABELS) as ClientIdentifierTyp[]).map(t => (
|
||||
<Select.Option key={t} value={t}>{CLIENT_IDENTIFIER_LABELS[t]}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="Wert"
|
||||
label="Kennung"
|
||||
rules={[{ required: true, message: 'Kennung eingeben' }]}
|
||||
tooltip="Groß-/Kleinschreibung sowie Leer- und Trennzeichen sind egal — sie werden beim Speichern vereinheitlicht."
|
||||
>
|
||||
<Input placeholder="z.B. DE123456789" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title="Neue Zuordnung" open={modalOpen} onOk={handleAdd} onCancel={() => setModalOpen(false)}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="UserId" label="User ID (Authentik)" rules={[{ required: true }]}>
|
||||
@@ -588,6 +704,12 @@ function DocTypesTab() {
|
||||
key: 'freigabe',
|
||||
render: (v: boolean | null) => v ? <Tag color="blue">Ja</Tag> : '—',
|
||||
},
|
||||
{
|
||||
title: 'E-Rechnungs-Codes',
|
||||
dataIndex: 'ERechnungTypeCodes',
|
||||
key: 'erechnung',
|
||||
render: (v: string | null) => v ? <Tag>{v}</Tag> : '—',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
@@ -637,6 +759,13 @@ function DocTypesTab() {
|
||||
<Form.Item name="FreigabeErforderlich" valuePropName="checked" label="Freigabe erforderlich">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="ERechnungTypeCodes"
|
||||
label="E-Rechnungs-Codes (BT-3)"
|
||||
tooltip="Rechnungstypen, die auf diese Belegart abgebildet werden – kommagetrennt. Üblich: 380 = Rechnung, 384 = korrigierte Rechnung, 381 = Gutschrift, 389 = Gutschriftverfahren. Leer lassen, wenn diese Belegart nie vorgeschlagen werden soll."
|
||||
>
|
||||
<Input placeholder="z.B. 380,384" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{editing && (
|
||||
@@ -655,7 +784,7 @@ function DocTypesTab() {
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
const ACTION_TYPE_LABELS: Record<number, string> = {
|
||||
1: 'Export (FTP/WebDAV)',
|
||||
1: 'Export (FTP)',
|
||||
2: 'Mail versenden',
|
||||
3: 'Tags setzen/entfernen',
|
||||
4: 'Custom Field setzen',
|
||||
@@ -1191,7 +1320,6 @@ function ExportTargetsTab() {
|
||||
<Form.Item name="Protocol" label="Protokoll" rules={[{ required: true }]}>
|
||||
<Select>
|
||||
<Select.Option value="ftp">FTP</Select.Option>
|
||||
<Select.Option value="webdav">WebDAV</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="Host" label="Host" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user