Tutorial: Mein erstes 1:n-Formular (Bestellung mit Positionen)¶
Diese Anleitung führt Formularentwickler ohne Vorerfahrung Schritt für Schritt durch den Bau eines Formulars mit einer 1:n-Beziehung: ein Kopf-Datensatz (eine Bestellung) mit beliebig vielen Detail-Zeilen (Bestellpositionen). Am Ende hast du ein funktionierendes Bestellformular mit Artikel-Auswahl und automatisch berechneten Summen.
Die vollständige Referenz steht unter Detail-Tabellen. Hier geht es um die „goldenen Pfade" — die häufigsten 80 %, ohne Sonderfälle. Wer lieber nur klickt, beginnt mit der bebilderten Kurzanleitung.
Was ist eine 1:n-Beziehung — und warum brauche ich sie?¶
„1:n" heißt: ein Hauptdatensatz hat viele Detail-Zeilen:
| Hauptdatensatz | Detail-Zeilen |
|---|---|
| eine Bestellung | beliebig viele Bestellpositionen |
| eine Reisekostenabrechnung | beliebig viele Belege |
| ein Kunde | beliebig viele Ansprechpartner |
| eine Rechnung | beliebig viele Rechnungspositionen |
Du weißt beim Anlegen noch nicht, wie viele Zeilen es werden. In KCForms
baust du das mit einem Feld vom Typ array und der Erweiterung
kc:childTable (Detail-Tabelle). Beim Speichern legt KCForms dafür
automatisch eine eigene Datenbank-Tabelle an und kümmert sich um Lesen und
Schreiben.
Vorbereitung¶
Hilfreich, aber nicht zwingend (jeweils 5–10 Minuten):
- Formular-Grundlagen — Felder, Typen, Layout.
- Datenmodell — wie Felder zu Datenbank-Spalten werden.
- Picker — für Schritt 4.
Du brauchst Zugriff auf den Formular-Editor (Zahnrad oben rechts → Formular-Editor). Fehlt der Eintrag, hat dein Konto keine Rechte für den Formular-Editor — frag deinen Admin.
Das Ziel¶
flowchart TB
K["<b>Kopf</b> — 1 Datensatz<br/>Name, Firma, Adresse, PLZ, Stadt, Telefon, E-Mail"]
P["<b>Bestellpositionen</b> — n Zeilen<br/>Bestellnr (Picker) · Beschreibung · Menge · Einzelpreis · Gesamtpreis (berechnet)"]
S["<b>Summen</b><br/>Zwischensumme (berechnet) · Versandart · Versandkosten · MwSt · Gesamt"]
K --> P --> S
Das mitgelieferte Beispiel „08 Bestellung" im Ausfüllmodus: Kopf-Felder
oben, darunter die Positionen, am Ende die Summen.
Schritt 1 — Neues Formular anlegen¶
- Formular-Editor öffnen (Zahnrad oben rechts → Formular-Editor).
- In der linken Spalte Formulare auf + Neu klicken.
- ID (URL-tauglich):
meine-bestellung— klein, ohne Umlaute; daraus wird der Tabellennameform_meine_bestellung. (Die IDbestellungist bereits vergeben: So heißt das mitgelieferte, fertige Beispiel „08 Bestellung".) - Titel: „Bestellung".
- Katalog (optional): z. B. „Tutorial".
- Erstellen.
Der Formular-Editor öffnet das neue, leere Formular.

Die ID bleibt
Die Formular-ID lässt sich nach dem Anlegen nicht mehr ändern — sie steckt im Tabellennamen. Lieber einmal kurz überlegen.
Schritt 2 — Die Kopf-Felder anlegen¶
Die einfachen Felder legst du komplett über die Oberfläche an. Zwei Wege:
- Baukasten: Reiter Baukasten an der rechten Leiste öffnen und aus Eingabefelder z. B. „Textfeld" oder „E-Mailadressfeld" in die Spalte Felder ziehen. Noch schneller: die Feldgruppe Anschrift aus Feldgruppen legt mehrere Adressfelder auf einmal an.
- + Neu in der Spalte Felder: Feldname, Typ, Spaltenbreite → Erstellen.
Anschließend per Doppelklick die Eigenschaften öffnen und Titel sowie Feld-ID einstellen:
| Feld-ID | Typ | Titel | Pflichtfeld? |
|---|---|---|---|
name |
Textfeld | Name | ja |
firma |
Textfeld | Firma | nein |
adresse1 |
Textfeld | Adresse 1 | nein |
adresse2 |
Textfeld | Adresse 2 | nein |
stadt |
Textfeld | Stadt | nein |
plz |
Textfeld (Maximallänge 10) | PLZ | nein |
land |
Textfeld | Land | nein |
telefon |
Textfeld | Telefon | nein |
email |
E-Mailadressfeld | nein |
In den Formular-Eigenschaften (Knopf Formulareigenschaften bearbeiten über der Vorschau) stellst du das Formularlayout auf Zwei Spalten — dann stehen die Felder paarweise nebeneinander.
Doppelklick auf ein Feld öffnet die Eigenschaften mit Reitern: Titel,
Feld-ID, Pflichtfeld, Länge usw.
Zwischendurch Speichern (oben rechts) nicht vergessen.
Schritt 3 — Die Detail-Tabelle anlegen (das Herzstück)¶
- Im Baukasten unter Eingabefelder einen der Einträge „Detail-Tabelle (1:n) · …" in die Felderliste ziehen, unterhalb der Kopf-Felder — oder über + Neu in der Spalte Felder den Typ Detail-Tabelle (1:n) wählen.
- Feld-ID
positionen, Titel „Bestellpositionen", Spaltenbreite volle Breite.
Die Spalten festlegen¶
Wähle positionen aus. In den Eigenschaften gibt es für Detail-Tabellen
den Bereich Felder mit der Liste Detailzeilen-Spalten. Über
+ Spalte legst du die Spalten einer Positionszeile an. Der Dialog fragt
zuerst nach dem Titel (der Spaltenüberschrift) und schlägt die Spalten-ID
daraus vor — überschreib sie mit dem kurzen Namen aus der Tabelle:

| Spalten-ID | Typ | Titel | Optionen |
|---|---|---|---|
bestellnr |
string | Bestellnr | Maximallänge 20 |
beschreibung |
string | Beschreibung | Maximallänge 200 |
menge |
number | Menge | 0 Nachkommastellen |
preis |
number | Einzelpreis | 2 Nachkommastellen, rechtsbündig |
summe |
number | Gesamtpreis | 2 Nachkommastellen, rechtsbündig, berechnet (Schritt 5) |
Leere Zeilen vorgeben¶
In den Eigenschaften steht unter den Spalten initialRows (Vorbelegung
leerer Zeilen). Mit 4 zeigt ein neuer Datensatz sofort vier leere
Zeilen.
Leere Zeilen werden mitgespeichert
Eine Detail-Tabelle speichert jede Zeile, die beim Speichern existiert —
auch eine komplett leere. Nicht benötigte Zeilen entfernt der Anwender
mit ×; wähle initialRows deshalb eher knapp.
Speichern. KCForms legt im Hintergrund die Tabelle
form_meine_bestellung_positionen an (Fremdschlüssel zur Bestellung plus je eine
Spalte pro Positionsfeld). Der Reiter Modell an der rechten Leiste
zeigt sie.
Schritt 4 — Die Bestellnummer mit einem Picker verbinden¶
Optional, aber beliebt: Der Anwender soll Artikelnummern nicht auswendig
tippen, sondern aus einer Liste wählen. Dafür bekommt die Spalte
bestellnr einen Picker. Doppelklick auf die Spalte in der Vorschau
→ Reiter Auswahl → Such-Dialog:

Im Schema sieht das so aus:
"kc:picker": {
"title": "Büromaterial auswählen",
"table": "bueromaterial",
"search": ["artikelnr", "bezeichnung", "kategorie"],
"columns": [
{ "column": "artikelnr", "title": "Artikelnr" },
{ "column": "bezeichnung", "title": "Bezeichnung" },
{ "column": "kategorie", "title": "Kategorie" },
{ "column": "preis", "title": "Preis" }
],
"orderBy": "artikelnr ASC",
"pageSize": 12,
"set": {
"bestellnr": "artikelnr",
"beschreibung": "bezeichnung",
"preis": "preis"
}
}
Das Wichtigste ist set: Wählt der Anwender einen Artikel, landen die
Werte in den Spalten derselben Zeile — artikelnr → bestellnr,
bezeichnung → beschreibung, preis → preis. Die Menge gibt der
Anwender selbst ein.
Zur Laufzeit öffnet das …-Symbol neben der Bestellnummer den Dialog:

Mehr dazu unter Picker.
Die Tabelle muss existieren
kc:picker.table verweist auf eine Tabelle in der Datenbank. In der
KCForms-Demo gibt es bueromaterial bereits; in einer eigenen
Installation nimmst du eine vorhandene Tabelle — oder eine externe
Datenquelle.
Schritt 5 — Gesamtpreis pro Zeile berechnen¶
summe soll automatisch menge × preis sein — dafür brauchst du kein
Skript. Doppelklick auf die Spalte Gesamtpreis → Reiter
Berechnung → Art Rechnung aus Feldern, Rechenart Produkt,
Operanden menge und preis.
Im Schema:
"summe": {
"type": "number", "title": "Gesamtpreis",
"kc:decimals": 2, "kc:align": "right",
"kc:calc": { "type": "arithmetic", "op": "multiply", "operands": ["menge", "preis"] }
}
Das Feld wird dadurch automatisch schreibgeschützt und rechnet live mit.
Schritt 6 — Zwischensumme über alle Zeilen¶
Lege ein Zahlenfeld zwischensumme (Titel „Zwischensumme",
2 Nachkommastellen) unter der Tabelle an. Reiter Berechnung → Art
Summe einer Tabellenspalte, Tabelle positionen, Spalte summe:
"zwischensumme": {
"type": "number", "title": "Zwischensumme",
"kc:decimals": 2, "kc:format": "currency-eur",
"kc:calc": { "type": "sum", "table": "positionen", "column": "summe" }
}
Details und Leerwert-Regeln: Berechnungsfelder.
Schritt 7 — Versand, MwSt und Gesamt¶
Berechnungsfelder kennen nur Felder als Operanden, keine festen Werte. Versandkosten nach Tarif und 19 % MwSt rechnest du deshalb in einem kleinen Skript:
versandart: Textfeld als Radio-Gruppe (kc:widget: "radio") mit fester Werteliste (kc:lookupOptions).versandkosten,mwst,gesamt: Zahlenfelder, schreibgeschützt, 2 Nachkommastellen.
Im Formular-Editor: Reiter Skripte über der Vorschau → Hook onFieldChange:

// onFieldChange — läuft, sobald der Anwender ein Feld verlässt.
var VERSAND = {
'post-inland-46': 5.95,
'post-inland-uebernacht': 24.95,
'post-inland-express-3': 7.95,
'post-inland-express-2': 12.95,
'international': 22.95,
'international-dhl': 39.95
};
// String(...) macht aus dem Java-Wert sicher einen JS-String für den Nachschlag.
var art = String(ctx.form.versandart || '');
var vk = VERSAND[art] || 0;
var zws = kc.num(ctx.form.zwischensumme);
ctx.form.versandkosten = vk;
ctx.form.mwst = (zws + vk) * 0.19;
ctx.form.gesamt = zws + vk + ctx.form.mwst;
kc.num(...) macht aus einem Feldwert sicher eine Zahl (leer → 0).
Mehr zu Skripten: Scripting-Überblick.
Berechnet wird im Browser
kc:calc rechnet im Web-Formular und speichert das Ergebnis mit.
Legt ein Skript oder ein REST-Service einen Datensatz ohne das
Formular an, rechnet dort niemand nach — siehe
Berechnungsfelder.
Schritt 8 — Testen¶
- Oben rechts Im Formular öffnen.
- Eine Position mit Menge 3 und Einzelpreis 5,00 eintragen.
- Feld verlassen → Gesamtpreis
15,00, Zwischensumme ebenfalls. - Versandart wählen → Versandkosten, MwSt und Gesamt aktualisieren sich.
- Speichern (Disketten-Symbol oder
Strg+S). - Mit + in der Werkzeugleiste einen zweiten Datensatz anlegen — beide werden getrennt gespeichert.
Logs des Backends und der Skripte findest du unter
System-Administration → Betrieb & Überwachung → System-Logs
(kcforms-backend.log, script.log).
Komplettes Beispiel¶
Das vollständige Schema — z. B. über Formular-Eigenschaften → JSON-Editor einfügen, um Schritt für Schritt zu vergleichen:
{
"type": "object",
"title": "Bestellung",
"kc:layout": "horizontal-2",
"properties": {
"name": { "type": "string", "title": "Name", "kc:required": true },
"firma": { "type": "string", "title": "Firma" },
"adresse1": { "type": "string", "title": "Adresse 1" },
"adresse2": { "type": "string", "title": "Adresse 2" },
"stadt": { "type": "string", "title": "Stadt" },
"plz": { "type": "string", "title": "PLZ", "kc:maxLength": 10 },
"land": { "type": "string", "title": "Land" },
"telefon": { "type": "string", "title": "Telefon" },
"email": { "type": "string", "title": "E-Mail", "format": "email" },
"positionen": {
"type": "array",
"title": "Bestellpositionen",
"kc:colspan": "full",
"kc:childTable": {
"initialRows": 4,
"properties": {
"bestellnr": {
"type": "string", "title": "Bestellnr", "kc:maxLength": 20,
"kc:picker": {
"title": "Büromaterial auswählen",
"table": "bueromaterial",
"search": ["artikelnr", "bezeichnung", "kategorie"],
"columns": [
{ "column": "artikelnr", "title": "Artikelnr" },
{ "column": "bezeichnung", "title": "Bezeichnung" },
{ "column": "kategorie", "title": "Kategorie" },
{ "column": "preis", "title": "Preis" }
],
"orderBy": "artikelnr ASC",
"pageSize": 12,
"set": { "bestellnr": "artikelnr", "beschreibung": "bezeichnung", "preis": "preis" }
}
},
"beschreibung": { "type": "string", "title": "Beschreibung", "kc:maxLength": 200 },
"menge": { "type": "number", "title": "Menge", "kc:decimals": 0 },
"preis": { "type": "number", "title": "Einzelpreis", "kc:decimals": 2, "kc:align": "right" },
"summe": { "type": "number", "title": "Gesamtpreis", "kc:decimals": 2, "kc:align": "right",
"kc:calc": { "type": "arithmetic", "op": "multiply", "operands": ["menge", "preis"] } }
}
}
},
"zwischensumme": { "type": "number", "title": "Zwischensumme",
"kc:decimals": 2, "kc:format": "currency-eur",
"kc:calc": { "type": "sum", "table": "positionen", "column": "summe" } },
"versandart": { "type": "string", "title": "Versandart", "kc:colspan": "full",
"kc:widget": "radio", "kc:lookupDisplay": "value",
"kc:lookupOptions": [
{ "key": "post-inland-46", "value": "Post Inland (4-6 Tage) — 5,95 €" },
{ "key": "post-inland-uebernacht", "value": "Post Inland Übernacht — 24,95 €" },
{ "key": "post-inland-express-3", "value": "Post Inland Express Spar (3 Tage) — 7,95 €" },
{ "key": "post-inland-express-2", "value": "Post Inland Express (2 Tage) — 12,95 €" },
{ "key": "international", "value": "International — 22,95 €" },
{ "key": "international-dhl", "value": "International DHL Express — 39,95 €" }
] },
"versandkosten": { "type": "number", "title": "Versandkosten",
"kc:readonly": true, "kc:decimals": 2, "kc:format": "currency-eur" },
"mwst": { "type": "number", "title": "MwSt (19 %)",
"kc:readonly": true, "kc:decimals": 2, "kc:format": "currency-eur" },
"gesamt": { "type": "number", "title": "Gesamt",
"kc:readonly": true, "kc:decimals": 2, "kc:format": "currency-eur" },
"bemerkungen": { "type": "string", "title": "Bemerkungen",
"kc:widget": "textarea", "kc:colspan": "full" }
}
}
Speichern und Drucken braucht das Schema nicht zu erwähnen — beide Knöpfe bekommt jedes Formular automatisch.
Checkliste — Fehlersuche¶
| Symptom | Wahrscheinliche Ursache |
|---|---|
| „Bestellpositionen" bleibt leer | type ist nicht array oder kc:childTable fehlt |
| Tabelle nur eine Spalte breit | kc:colspan fehlt oder ist nicht "full" |
| Gesamtpreis rechnet nicht | kc:calc an summe fehlt, oder die Operanden-IDs stimmen nicht mit den Spalten-IDs überein |
| Zwischensumme bleibt leer | kc:calc.table/column zeigen auf eine falsche Tabelle oder Spalte |
| Versandkosten bleiben 0 | Schlüssel in VERSAND stimmen nicht mit den key-Werten der Werteliste überein |
| Picker meldet einen Tabellenfehler | kc:picker.table verweist auf eine nicht vorhandene Tabelle |
| Fehler „column does not exist" beim Speichern | Spalte umbenannt, alte DB-Spalte existiert noch → siehe Detail-Tabellen → Stolperfallen |
| Leere Zeilen im Ausdruck oder in der Liste | Leere Zeilen werden mitgespeichert — mit × entfernen oder initialRows senken |
Wie geht es weiter?¶
- Picker — Such-Spalten, Abhängigkeiten, Skript-Picker.
- Berechnungsfelder — alle Rechenarten und Verkettungen.
- Validierung — Pflichtfelder, Muster,
ctx.addError(). - Druck — Detail-Tabellen erscheinen automatisch im PDF und laufen bei Bedarf über mehrere Seiten.
- Detail-Tabellen (Referenz) — alle Einstellungen im Detail, inklusive Block-Ansicht und Verschachtelung.
