Zum Inhalt springen
Zurück zur Knowledge Base

MIS-Daten auf das CoCoCo-Datenmodell abbilden

Gilt für die CoCoCo-Plattform v1.0.0-rc.31. Jede Aussage unten wurde auf dieser Version geprüft; das SQL-Server-Beispiel ließ sich dort nicht ausführen (kein SQL Server auf dem Testmandanten), und der Hinweis zu festen CHAR-Spalten wurde nicht nachgestellt.

Zeilen aus einer MIS-Datenbank zu lesen ist die leichte Hälfte. Die Arbeit steckt in der Frage, zu welchem Datensatz in CoCoCo eine Zeile gehört — bei jedem Lauf, ohne Dubletten und ohne eigene Zuordnungstabelle.

Genau dafür hat CoCoCo drei Mechanismen. Einmal verstanden, sieht jede weitere Integration gleich aus.

Kunden, Aufträge, Jobs und Arbeitsgänge tragen jeweils eine externalId: die Kennung, die der Datensatz in Ihrem Quellsystem hat. Beim Anlegen mitschreiben — und die Nummerierung des Quellsystems wird der Schlüssel, über den Sie Datensätze wiederfinden.

Verwenden Sie ein Präfix, damit die Herkunft sichtbar bleibt, und halten Sie die Quellnummer lesbar:

Datensatz in der QuelleexternalId
Kunde 1042MIS:K-001042
Auftrag 26-04711MIS:26-04711
Arbeitsgang 20 dieses AuftragsMIS:26-04711:20

Das Importmuster ist dann immer dasselbe — erst suchen, nur bei Fehlen anlegen:

local ok, res = ctx.graphql.query([[
query($e: String!) {
listJobs(first: 1, filter: { externalId: { eq: $e } }) { edges { node { id } } }
}]], { e = "MIS:26-04711" })
if not ok then error(res) end
local edge = res.data.listJobs.edges[1] -- nil, wenn der Job noch nicht existiert

ctx.graphql.query liefert zwei Werte: ob der Aufruf durchging, und das Ergebnis. Wer ihn einer einzigen Variablen zuweist, behält nur den ersten — true — und die Suche sieht nie einen Job.

Kommt der Job zurück, verwenden Sie seine ID. Kommt er nicht, legen Sie ihn mit dieser externalId an. Ein zweiter Lauf desselben Imports erzeugt dann gar nichts — genau das, was man will, wenn ein Timer alle fünf Minuten feuert.

Trimmen Sie Ihre Quellwerte, bevor Sie sie als Kennung verwenden. Feste CHAR-Spalten kommen mit Leerzeichen aufgefüllt an, und "26-04711 " ist eine andere Kennung als "26-04711".

2. externalRef — Fortschritt buchen, ohne CoCoCo-IDs zu kennen

Abschnitt betitelt „2. externalRef — Fortschritt buchen, ohne CoCoCo-IDs zu kennen“

Rückmeldungen aus der Produktion sind in jeder MIS-Integration die größte Datenmenge — und die Stelle, an der eine Zuordnungstabelle am meisten schmerzt. recordProgressPoints nimmt deshalb statt einer Arbeitsgang-ID eine externalRef und löst das Ziel über Ihre eigenen Koordinaten auf:

local ok, res = ctx.graphql.query([[
mutation($i: RecordProgressPointsInput!) {
recordProgressPoints(input: $i) { recorded errors { message } }
}]], { i = { points = { {
externalRef = {
jobExternalId = "MIS:26-04711",
operationExternalId = "MIS:26-04711:20",
},
timestamp = "2026-09-04T06:41:00Z",
phase = "RUNNING",
goodCount = 1420,
wasteCount = 95,
source = "MANUAL",
} } } })
if not ok then error(res) end
-- res.data.recordProgressPoints.errors nennt die Punkte, die sich nicht auflösen ließen

Kein Lookup, keine ID-Übersetzung. Die Auflösung läuft von oben nach unten (Job → Komponente → Arbeitsgang → Position), Teilkoordinaten funktionieren also auch.

Setzen Sie source ehrlich: MANUAL für eine Eingabe am Terminal durch einen Menschen, JMF für eine Maschinenmeldung. Wer die Daten liest, erkennt daran, ob eine Zahl gemessen oder getippt wurde.

3. Ein Wasserzeichen — woher man weiß, was neu ist

Abschnitt betitelt „3. Ein Wasserzeichen — woher man weiß, was neu ist“

Fragen Sie die Datenbank bei jedem Lauf nur nach dem, was sich geändert hat. Welche Spalte dafür taugt, hängt davon ab, was die Tabelle hergibt:

Die Tabelle hatWasserzeichen
Einen ÄnderungszeitstempelDen höchsten bereits importierten Zeitstempel
Eine fortlaufende ID, keinen ZeitstempelDie höchste bereits importierte ID
Keins von beidemEin festes Fenster erneut lesen (z. B. 48 Stunden). Die externalId-Suche fängt wiederholte Kunden, Aufträge, Jobs und Arbeitsgänge ab — keine Fortschrittspunkte (siehe unten)

Das Wasserzeichen gehört in ctx.cache — ein Zeiger, nie die Daten:

ctx.cache.set({ key = "mis:rueckmeldung:letzte", value = tostring(hoechste), ttl = 604800 })

Dazu zwei Regeln, die echte Mühe ersparen:

  • Das Wasserzeichen nur nach einem fehlerfreien Lauf fortschreiben. Für Kunden, Aufträge, Jobs und Arbeitsgänge ist ein wiederholter Lauf harmlos: die externalId-Suche findet, was der letzte Lauf angelegt hat. Fortschrittspunkte haben keinen solchen Schlüssel — ein zweimal gesendeter Punkt wird zweimal gespeichert und lässt sich nicht löschen. Bei Teilfehlern senden Sie nur die Rückmeldungen erneut, die nicht gespeichert wurden (recordProgressPoints antwortet mit der Zahl recorded und den errors).
  • Kaltstart beherrschen. Der Cache kann verfallen. Fehlt der Zeiger, importieren Sie nicht alles von Anfang an: für Stammdaten ein festes Fenster erneut lesen, für Rückmeldungen nach dem jüngsten schon gespeicherten Punkt des Arbeitsgangs beginnen.

Viele MIS-Schemas halten Datum und Uhrzeit in getrennten Spalten. Setzen Sie sie in der Abfrage zusammen, nicht im Skript — und rechnen Sie nach UTC um, bevor Sie das Z anhängen. Ein BDE-Terminal schreibt die Ortszeit; so wie sie ist als UTC ausgewiesen, liegt in Mitteleuropa jeder Punkt ein bis zwei Stunden in der Zukunft:

-- PostgreSQL: über die Zeitzone der Datenbank selbst
to_char(((beginn_datum + beginn_zeit::time) AT TIME ZONE current_setting('TimeZone')) AT TIME ZONE 'UTC',
'YYYY-MM-DD"T"HH24:MI:SS') || 'Z'
-- SQL Server: die Zeitzone nennen, in der das MIS schreibt
FORMAT((DATEADD(second, DATEDIFF(second, 0, beginn_zeit), CAST(beginn_datum AS datetime2))
AT TIME ZONE 'W. Europe Standard Time') AT TIME ZONE 'UTC', 'yyyy-MM-ddTHH:mm:ss') + 'Z'

Speichert Ihr MIS bereits UTC, lassen Sie die Umrechnung weg. Fragen Sie den Hersteller; die Spaltennamen verraten es selten.

Zwei Gründe, es in SQL zu tun. Eine DATE-Spalte kommt als vollständiger Zeitstempel an (2026-09-02T00:00:00Z), nicht als 2026-09-02 — eine angehängte Uhrzeit ergibt also eine unbrauchbare Zeichenfolge. Und ein falscher Zeitstempel ist teuer: Fortschrittspunkte sind append-only. Eine falsch datierte Produktionshistorie lässt sich nicht korrigieren, nur ergänzen, und latestProgress meldet den jüngsten Punkt.

Statuscodes und Kostenstellen brauchen eine Übersetzung, und die gehört in die Konfiguration. STATUS = 2 oder KST-OFF bedeutet für CoCoCo nichts; Sie bilden es auf CONFIRMED oder OFFSET_PRINTING ab. Die Codes unterscheiden sich von Kunde zu Kunde — halten Sie die Tabelle an einer Stelle Ihrer Integrationskonfiguration, nicht über den Code verstreut.

Einheiten sind typisiert. Für Bogen und Drucke gilt countUnit: CUSTOM mit customCountUnit: "SHEETS", geschrieben genau so, wie verticalProductionUnits es führt — die Zeichenfolge ist Freitext, ein Tippfehler erzeugt still eine zweite Reihe.

Für ein Druck-MIS fällt die Abbildung meist so aus:

MISCoCoCo
KundenstammKunde (externalId, Name, Zahlungsbedingungen)
AuftragskopfAuftrag (orderNumber, externalId, Termine, Beträge)
AuftragspositionenAuftragspositionen; die Produktionsposition trägt jobId
Auftrag + Auflage + LieferterminJob (quantity, dueAt, jobNumber)
ArbeitsgängeArbeitsgänge (kind aus der Kostenstelle, requestedWorkCenterId aus der Maschine)
RückmeldungenFortschrittspunkte über externalRef

Zeigt die Auftragsposition auf den Job, liefert orderLifecycle Auftrag, Positionen und Produktionsjob in einem Aufruf.

  • Eine externe Datenbank (MIS/ERP) über SQL anbinden — die Verbindung überhaupt erst herstellen
  • Was sind Integrationen? und Eine Integration bauen — wohin dieser Code gehört, sobald er auf einem Timer laufen soll statt von Hand
  • Lua Playground: Erste Schritte und Skripte verwenden — um die Abbildung erst einmal auszuprobieren
  • API-Tokens erstellen und verwalten — wenn Sie den Import von außen fahren statt aus einem Skript