Zum Inhalt springen

This page is available in English. Switch to English

Arbeit, die jede Woche wiederkommt? Muss kein Mensch machen. Mehr zu KI und Automatisierung

Automatisierung

Akeneo: Einfache Produkte zu einem Produktmodell zusammenführen, ohne Inhalte zu verlieren

Gravierte Metallteile nebeneinander in einem langen Rahmen, daneben liegen lose Teile.

Varianten landen in Akeneo oft in der falschen Form. Ein Import aus dem alten Shop, aus einer Tabelle oder aus dem ERP legt jede Größe eines T-Shirts als eigenes einfaches Produkt an. Jedes hat einen eigenen Namen, eine eigene Beschreibung, eigene Bilder und eigene Kategorien. Später richtet jemand eine Familienvariante ein und möchte die Produkte unter einem Produktmodell bündeln.

Genau so war es bei mir. Die einfachen Produkte hatten alle Daten, und die Daten stimmten. Neu eintippen kam nicht in Frage, und sie zu verlieren schon gar nicht.

Was die Sammelaktion tatsächlich tut

Akeneo hat dafür eine Sammelaktion: „Zu einem bestehenden Produktmodell hinzufügen“. Sie hat zwei Haken.

Erstens muss das Produktmodell schon existieren. Man legt es also vorher von Hand an, und es ist leer.

Zweitens, und das ist der teure Teil: Sobald die Produkte dem Modell zugeordnet werden, entfernt Akeneo laut eigener Dokumentation die bisherigen Werte aller Attribute, die in der Familienvariante als gemeinsam definiert sind. Die Produkte übernehmen stattdessen den Wert des Modells, und der ist leer.

Die Reihenfolge lautet also: leeres Modell anlegen, Produkte zuordnen, die vorhandenen Beschreibungen verlieren, Beschreibungen auf Modellebene neu schreiben.

Bei drei Größen eines T-Shirts ist das lästig. Bei einigen hundert Produkten sind es Wochen.

Ich wollte die umgekehrte Reihenfolge. Zuerst das Produktmodell aus den vorhandenen Werten der einfachen Produkte bauen, dann die Produkte zuordnen. Dann muss nichts neu geschrieben werden, weil die Inhalte schon auf dem Modell liegen, wenn die Produkte dazukommen.

Das Ganze läuft gegen Akeneo PIM Community Edition 6.0, über die REST-API, gesteuert mit n8n.

Die Eingabe

Der Workflow wird von Hand gestartet, einmal pro Produktmodell. Er bekommt ein kleines JSON-Objekt:

{
  "codes": {
    "family": "t_shirt",
    "variation": {
      "code": "t_shirt_by_size",
      "common_attributes": ["name", "description", "care_instructions", "material", "main_image"]
    },
    "product_model": "tshirt-basic",
    "variants": ["tshirt-basic-s", "tshirt-basic-m", "tshirt-basic-l"]
  }
}

Die Liste der gemeinsamen Attribute wiederholt, was die Familienvariante schon festlegt.

Exakt zur Familienvariante passen. Der Workflow liest die gemeinsamen Attribute nicht aus Akeneo. Die Liste muss also exakt zur Familienvariante passen, sonst übernimmt der Merge die falschen Werte.

Davor sitzt ein kleiner Umschalter zwischen dev und prod, der die passende Basis-URL und den passenden API-Client lädt. Stellen Sie dev als Standard ein. Dann schadet es nicht, wenn jemand vergisst umzuschalten.

Schritt 1: nichts überschreiben

Der Workflow fragt Akeneo nach dem Code des Produktmodells. Existiert es schon, bricht er mit einem Fehler ab. Ein zweiter Lauf darf nie neue Produkte in ein Modell mischen, das inzwischen jemand bearbeitet hat.

Schritt 2: keine unvollständigen Produkte

Dann lädt er jedes einfache Produkt. Zwei Dinge beenden den Lauf:

  1. Ein Produktcode, den es nicht gibt.
  2. Ein Produkt mit einer Vollständigkeit unter 100 in irgendeiner benötigten Sprache im Kanal ecommerce.

Die zweite Regel ist wichtiger, als sie aussieht. Der ganze Sinn ist, vorhandene Inhalte zu erhalten. Ist ein Produkt unvollständig, entsteht das Modell aus lückenhaften Werten, und nach der Zuordnung bleiben die Lücken. Deshalb startet der Workflow erst, wenn jede Variante vollständig ist:

// Gekürzt. Läuft einmal über alle geladenen Produkte.
const locales = ['en_US', 'de_DE', 'fr_FR'];
const scope = 'ecommerce';
const missing = [];
const incomplete = [];

for (const { json } of $input.all()) {
  const id = json.identifier || json.code || '[unknown]';
  if (json.error?.status === 404 || json.missing === true) { missing.push(id); continue; }

  for (const locale of locales) {
    const score = (json.completenesses || [])
      .find(c => c.scope === scope && c.locale === locale)?.data ?? null;
    if (score !== 100) incomplete.push(`${id} ${locale}: ${score ?? 'missing'}`);
  }
}

if (missing.length || incomplete.length) {
  return [{ json: { error: [...missing.map(m => `Missing: ${m}`), ...incomplete].join('\n') } }];
}
return $input.all();

Das n8n-Detail, an dem alles hängt

Der Fehlerfall wirft keinen Fehler. Er gibt ein Item zurück, dessen JSON genau einen Schlüssel hat: error. Beim Code-Node steht „On Error“ auf dem Fehlerausgang, und der ist mit einem Stop-and-Error-Node verbunden.

Das funktioniert wegen einer Regel in der Ausführungslogik von n8n. Nutzt ein Node den Fehlerausgang, verschiebt n8n jedes zurückgegebene Item dorthin, dessen JSON nur den Schlüssel error enthält, oder error und message. In n8n 2.7.1 steht das in handleNodeErrorOutput in workflow-execute.ts.

Das ist auch zerbrechlich. Fügt jemand ein weiteres Feld hinzu, etwa die Anzahl der fehlenden Produkte, erfüllt das Item die Regel nicht mehr. n8n schickt es dann über den Erfolgsausgang weiter, und der Workflow baut ein Produktmodell aus unvollständigen Produkten. Wer das Muster übernimmt, sollte das Fehlerobjekt bei diesem einen Schlüssel belassen oder gleich einen Fehler werfen.

Schritt 3: die gemeinsamen Werte zusammenführen

Das ist der Kern. Für jedes gemeinsame Attribut entscheidet der Workflow, welcher Produktwert zum Wert des Modells wird. In einem sauberen Katalog sind die Werte gleich. In einem echten nicht immer.

Die Regel:

  1. Das Produkt mit Werten in den meisten Sprachen für dieses Attribut gewinnt.
  2. Bei Gleichstand gewinnt die höhere durchschnittliche Vollständigkeit.

Akeneo-Attribute gibt es in vier Formen, und jede wird anders gelesen:

AttributtypBeispielGelesen nach
Lokalisierbar und kanalspezifischdescriptionSprache und Kanal
Nur lokalisierbarcare_instructionsSprache
Nur kanalspezifischmain_imageKanal
Weder nochmaterialerster Wert des vollständigsten Produkts
// Gekürzt: die Auswahl pro Attribut.
const candidates = products
  .filter(p => Array.isArray(p.values?.[attr]))
  .map(p => {
    const byLocale = {};
    for (const locale of locales) {
      const v = p.values[attr].find(e => e.locale === locale && e.scope === scope);
      if (v && v.data !== '' && v.data != null) byLocale[locale] = v.data;
    }
    return { product: p, byLocale, filled: Object.keys(byLocale).length, completeness: avgCompleteness(p) };
  })
  .sort((a, b) => b.filled - a.filled || b.completeness - a.completeness);

const best = candidates[0];

Kategorien sind einfacher. Das Modell bekommt die Vereinigung aller Kategorien der Produkte, ohne Duplikate.

Schritt 4: Modell anlegen, dann Produkte zuordnen

Mit den zusammengeführten Werten schickt der Workflow einen POST für das Produktmodell:

{
  "code": "tshirt-basic",
  "family": "t_shirt",
  "family_variant": "t_shirt_by_size",
  "categories": ["apparel", "summer_2026"],
  "values": { "description": [{ "locale": "de_DE", "scope": "ecommerce", "data": "…" }] }
}

Er wartet, bis diese Anfrage fertig ist, und setzt dann per PATCH bei jedem einfachen Produkt das neue Elternmodell:

{ "parent": "tshirt-basic" }

Ab diesem Moment liest jedes Produkt seine gemeinsamen Attribute vom Modell, und das Modell enthält bereits die Werte, die die Produkte hatten.

Was schiefgehen konnte

Die erste Version konnte still scheitern. Der POST, der das Produktmodell anlegt, hatte seinen Fehlerausgang aktiviert, aber nichts war daran angeschlossen. Hätte Akeneo das Modell abgelehnt, wäre der Zweig einfach zu Ende gewesen. Kein Produkt zugeordnet, nichts geschrieben, und n8n hätte die Ausführung als erfolgreich angezeigt. Eine spätere Version hängt einen Stop-and-Error-Node an diesen Ausgang.

Die Lehre gilt für jeden n8n-Workflow: Jeder aktivierte Fehlerausgang braucht etwas am anderen Ende.

Ein offener Fehlerausgang ist eine Methode, Fehler verschwinden zu lassen.

Was der Workflow noch nicht abdeckt

  • Konflikte werden still entschieden. Haben zwei Größen unterschiedliche Beschreibungen, gewinnt eine nach der Regel oben, und niemand erfährt, dass es die andere gab. Prüfen Sie eine Stichprobe.
  • Jeder Fehler bei der Abfrage gilt als „Modell nicht gefunden“. Die Existenzprüfung schickt jeden Fehler von Akeneo weiter, nicht nur einen 404. Ein Timeout oder ein 500 lässt den Lauf also weiterlaufen.
  • Es gibt kein Zurückrollen. Wird das Modell angelegt und scheitert der dritte PATCH, sind zwei Produkte zugeordnet und eines nicht. Ein erneuter Lauf stoppt bei „Modell existiert bereits“. Den Rest macht man von Hand.
  • Ein Modell pro Lauf. Das ist ein Werkzeug zum Aufräumen, keine Migrationsstrecke. Für Hunderte Modelle gehört dieselbe Logik in eine Schleife mit einem Bericht am Ende.
  • Die Fehlermeldung beim gescheiterten Anlegen lautet noch „Problem found with variants“, obwohl sie beim gescheiterten POST des Produktmodells auslöst. Benennen Sie sie beim Übernehmen um.

Solange der Workflow von Hand läuft, ein Modell nach dem anderen, bleiben diese Punkte beherrschbar. Wichtig werden sie in dem Moment, in dem jemand ihn zeitgesteuert laufen lässt.

Wann sich das lohnt

Bei einer Handvoll Produkte: Sammelaktion nutzen und die Beschreibungen neu schreiben. Das geht schneller, als irgendetwas davon aufzusetzen.

Kam Ihr Katalog flach an, und steckt in jeder Variante echter Inhalt, dann gehen beim Umziehen von Hand die Wochen verloren. Für diesen Fall ist der Workflow gebaut.

Wenn Ihre Produktdaten so aussehen und Sie das nicht selbst bauen möchten, ist das genau meine Arbeit.