Merchgain
DE
Jetzt verfügbar
Als Nächstes, in dieser Reihenfolge
  • FrançaisFR
  • ItalianoIT
  • PortuguêsPT
  • NederlandsNL

Die deutschen Texte hat noch kein Muttersprachler geprüft. Fehler gefunden? Schreiben Sie uns.

Kostenlos installierenInstallieren

Ihre Kosten sind schon da, nur im falschen Feld

Die Arbeit haben Sie schon gemacht. Die Werte stehen in einem Feld, von dem der WooCommerce-Kern nichts weiß, und der Bericht zeigt eine Lücke, wo Ihre Daten liegen. Das Skript auf dieser Seite überträgt sie, nachdem es Ihnen genau gezeigt hat, was es tun würde.

Aktualisiert 2026-09-24

Hat Ihnen der Hinweis zu fehlenden Kosten gesagt, dass für einige Ihrer Produkte Kosten in einem anderen Plugin hinterlegt sind, dann ist dies die Seite, auf die er verwiesen hat. Nichts hier ist spezifisch für Merchgain: Das Skript schreibt in das Feld, das der WooCommerce-Kern bereitstellt, und funktioniert deshalb gleich, ob Sie unser Plugin behalten oder nicht.

Diese Seite setzt voraus, dass die Funktion „Kosten der verkauften Waren“ bereits eingeschaltet ist. Ist sie es nicht, schalten Sie sie zuerst ein: Solange der Schalter aus ist, erreichen Werte im Feld des Kerns keine einzige Bestellung.

Das Übertragen repariert die Zukunft, nicht die Vergangenheit. WooCommerce schreibt den Wareneinsatz im Moment des Verkaufs in die Bestellposition. Bestellungen vor der Übertragung bleiben also für immer ohne Wareneinsatz. Nichts wird nachträglich ergänzt, und das ist kein Versehen. Das ist ein Grund, die Übertragung diese Woche zu erledigen statt im nächsten Quartal.

Welche Felder das Skript liest

Wo der Wareneinsatz stehtPlugin (Originalname)
_alg_wc_cog_costCost of Goods: Product Cost & Profit Calculator for WooCommerce (WPFactory)
_wc_cog_costWooCommerce Cost of Goods (SkyVerge, verkauft auf woocommerce.com)
_wcj_purchase_priceBooster for WooCommerce, Modul „Cost of Goods“
_izettle_cost_priceBjornTech PayPal POS integration for WooCommerce
_ni_cost_goodsNi Cost of Goods for WooCommerce
cost_of_goodsCost Of Goods For WooCommerce
Tabelle atum_product_data, Spalte purchase_priceATUM Inventory Management for WooCommerce

Diese Stellen haben wir im Code der Plugins selbst gefunden, an der Zeile, an der jedes den Wert speichert, nicht in einer Konstante, einer Formularbeschriftung oder einem Changelog. Diese Unterscheidung ist keine Pedanterie, und sie hat auf dieser Seite schon mehr als einen Fehler verhindert. Das Plugin von WPFactory kündigte eine Version mit „prefix updated to wpfcogs“ an, die das Präfix seiner Funktionen und Optionen änderte und nicht den Meta-Schlüssel. ATUM nennt sein Formularfeld _purchase_price, führt den Wert seit Version 1.5.0 (Dezember 2018) aber in einer eigenen Tabelle. Ein Skript, das nach einer dieser Angaben geschrieben wäre, würde ein Feld suchen, das es nicht gibt, und eine glatte Null melden. Die einzige Ausnahme ist WooCommerce Cost of Goods: eine kostenpflichtige Erweiterung ohne öffentlichen Code. Ihr Schlüssel stammt deshalb aus ihrer eigenen Dokumentation, bestätigt durch die Plugins, die daraus importieren.

Korrektur vom 24. September 2026. Bis zu diesem Tag las dieses Skript das Meta-Feld _purchase_price von ATUM. Dorthin schreibt ATUM seit 1.5.0 nicht mehr: In einem Shop, der ATUM schon früher nutzte, sind diese Zeilen Kopien, eingefroren am Tag, an dem ATUM seine Daten in die eigene Tabelle verlegt hat, und in einem neueren Shop gibt es sie gar nicht. Haben Sie das frühere Skript in einem Shop mit ATUM ausgeführt und hat es etwas übertragen, vergleichen Sie diese Kosten mit den Einkaufspreisen in ATUM, bevor Sie sich auf den Bericht verlassen.

Zu zwei davon ein eigener Hinweis. Booster kann zum Einkaufspreis „zusätzliche Ausgaben“ und eigene Kostenfelder hinzufügen; das Skript überträgt nur den Einkaufspreis, weil Versand und ähnliche Extras kein Wareneinsatz sind. Zählen Sie sie bei sich zum Wareneinsatz, ergänzen Sie sie von Hand. WooCommerce Cost of Goods kopiert seine Kosten laut Changelog ab Version 2.15.0 selbst in das Feld des Kerns; in einem Shop, der es nutzt, meldet das Skript also überwiegend Kosten, die schon dort stehen.

Liegen Ihre Kosten woanders, stimmt der Aufbau des Skripts trotzdem; ergänzen Sie Ihren Schlüssel in der Liste oben und prüfen Sie vor dem Ausführen, was der Wert bedeutet: netto oder brutto, je Artikel oder je Position. Ein Kostenfeld, das sich als Speicher für Bruttopreise herausstellt, ist schlimmer als gar keines.

Das Skript

Es liest und entscheidet, schreibt aber nichts, bis Sie die Zeile $DRY_RUN ändern. Speichern Sie es als migrate-costs.php und führen Sie es mit WP-CLI aus.

<?php
/**
 * Überträgt Kosten aus einem anderen Plugin in das Feld „Wareneinsatz“ von WooCommerce.
 * Aufruf:  wp eval-file migrate-costs.php
 */

$DRY_RUN = true;   // <- erst nach einem erfolgreichen Probelauf auf false setzen

// Felder in post meta: Meta-Schlüssel => Plugin.
$KEYS = array(
    '_alg_wc_cog_cost'    => 'Cost of Goods: Product Cost & Profit Calculator for WooCommerce',
    '_wc_cog_cost'        => 'WooCommerce Cost of Goods',
    '_wcj_purchase_price' => 'Booster for WooCommerce',
    '_izettle_cost_price' => 'BjornTech PayPal POS integration for WooCommerce',
    '_ni_cost_goods'      => 'Ni Cost of Goods for WooCommerce',
    'cost_of_goods'       => 'Cost Of Goods For WooCommerce',
);

// Plugins mit eigener Tabelle: Tabelle ohne Präfix => ID-Spalte, Kostenspalte.
$TABLES = array(
    'atum_product_data' => array( 'product_id', 'purchase_price' ),   // ATUM Inventory Management for WooCommerce
);

global $wpdb;

$found = array();

$placeholders = implode( ', ', array_fill( 0, count( $KEYS ), '%s' ) );
$rows         = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT post_id, meta_key, meta_value
           FROM {$wpdb->postmeta}
          WHERE meta_key IN ( {$placeholders} )
            AND meta_value <> ''
            AND CAST( meta_value AS DECIMAL( 20, 6 ) ) > 0",
        array_keys( $KEYS )
    ),
    ARRAY_A
);
foreach ( (array) $rows as $row ) {
    $found[ (int) $row['post_id'] ][ $row['meta_key'] ] = $row['meta_value'];
}

foreach ( $TABLES as $table => $columns ) {
    list( $id_column, $cost_column ) = $columns;
    $name = $wpdb->prefix . $table;
    if ( $name !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $name ) ) ) ) {
        continue;                       // dieses Plugin war hier nie installiert
    }
    $rows = $wpdb->get_results(
        "SELECT {$id_column} AS id, {$cost_column} AS cost
           FROM {$name}
          WHERE {$cost_column} IS NOT NULL
            AND CAST( {$cost_column} AS DECIMAL( 20, 6 ) ) > 0",
        ARRAY_A
    );
    foreach ( (array) $rows as $row ) {
        $found[ (int) $row['id'] ][ "{$table}.{$cost_column}" ] = $row['cost'];
    }
}

$moved = 0;
$owned = 0;
$parts = 0;
$held  = 0;
$clash = 0;
$gone  = 0;

foreach ( $found as $id => $values ) {
    $product = wc_get_product( $id );
    if ( ! $product ) {
        WP_CLI::warning( "No product with id {$id} - leftover data from something deleted." );
        $gone++;
        continue;
    }

    // Der Wareneinsatz einer Variante kann den des übergeordneten Produkts ersetzen
    // oder zu ihm addiert werden. Das ist eine Entscheidung darüber, was die Zahl bedeutet,
    // also lässt das Skript ihn in Ruhe.
    if ( $product->is_type( 'variation' ) ) {
        WP_CLI::log( sprintf( '[variation] %d %s: %s - left for you',
            $id, $product->get_name(), implode( ', ', $values ) ) );
        $parts++;
        continue;
    }

    $current = $product->get_cogs_value();
    if ( null !== $current && '' !== $current ) {
        $owned++;                       // nie einen Wareneinsatz überschreiben, der schon im Feld des Kerns steht
        continue;
    }

    // Als Zahlen verglichen: "12.50" in einem Feld und "12.5" in einem anderen sind derselbe Wert.
    $numbers = array();
    foreach ( $values as $value ) {
        $numbers[] = (string) round( (float) $value, 6 );
    }
    if ( count( array_unique( $numbers ) ) > 1 ) {
        $shown = array();
        foreach ( $values as $key => $value ) {
            $shown[] = "{$key}={$value}";
        }
        WP_CLI::warning( sprintf( 'Two costs disagree on %d %s: %s - skipped',
            $id, $product->get_name(), implode( ', ', $shown ) ) );
        $clash++;
        continue;
    }

    $cost = (float) reset( $values );

    // Den Wareneinsatz eines variablen Produkts übernimmt in WooCommerce jede Variante
    // ohne eigenen Wert. Hat eine davon im alten Plugin einen ANDEREN Wert - oder eine
    // ausdrückliche Null im selben Feld -, bekäme sie durch das Übertragen unbemerkt den
    // Wert des übergeordneten Produkts. Ein solches Produkt wird zurückgehalten, bis seine
    // Varianten eingestellt sind.
    if ( $product->is_type( 'variable' ) ) {
        $differ = array();
        foreach ( $product->get_children() as $child_id ) {
            $child = wc_get_product( $child_id );
            if ( ! $child || null !== $child->get_cogs_value() ) {
                continue;               // Variante mit eigenem Wert in WooCommerce erbt nichts - überspringen
            }
            $theirs = isset( $found[ $child_id ] ) ? $found[ $child_id ] : array();
            foreach ( array_keys( $values ) as $key ) {
                $zero = isset( $KEYS[ $key ] ) ? get_post_meta( $child_id, $key, true ) : '';
                if ( '' !== $zero && 0.0 === (float) $zero ) {
                    $theirs[ $key ] = '0';
                }
            }
            foreach ( $theirs as $value ) {
                if ( (string) round( (float) $value, 6 ) !== (string) round( $cost, 6 ) ) {
                    $differ[] = $child_id;
                    break;
                }
            }
        }
        if ( array() !== $differ ) {
            WP_CLI::warning( sprintf( 'Variable product %d %s: variations %s have a different cost in the old plugin, and moving %s would give them this one - held back, set those variations first',
                $id, $product->get_name(), implode( ', ', $differ ), $cost ) );
            $held++;
            continue;
        }
    }

    if ( $DRY_RUN ) {
        WP_CLI::log( sprintf( '[dry run] %d %s: -> %s (from %s)',
            $id, $product->get_name(), $cost, key( $values ) ) );
    } else {
        $product->set_cogs_value( $cost );
        $product->save();
        WP_CLI::log( sprintf( '%d %s = %s', $id, $product->get_name(), $cost ) );
    }
    $moved++;
}

WP_CLI::success( sprintf(
    '%s: %d | already had a cost: %d | variations left for you: %d | held back: %d | disagreements: %d | missing products: %d',
    $DRY_RUN ? 'To move' : 'Moved', $moved, $owned, $parts, $held, $clash, $gone
) );
wp eval-file migrate-costs.php

Das Skript steht hier zum Lesen, nicht zum Herunterladen mit einem Klick, und das mit Absicht. Es schreibt in Ihre Datenbank, also sollten Sie es lesen, bevor es Ihre Produkte ändert.

So lesen Sie den Probelauf

Die letzte Zeile ist der ganze Bericht, und jede Zahl darin bedeutet etwas, worauf Sie vielleicht reagieren müssen. Das Skript meldet auf Englisch:

  • To move (zu übertragen): Produkte, die einen Wareneinsatz bekommen. Nur diese Zahl ändert etwas.
  • already had a cost (hatten schon einen Wareneinsatz): Das Feld des Kerns ist gefüllt, also hält sich das Skript heraus. Deckt diese Zahl Ihren ganzen Katalog ab und bleibt nichts zu übertragen, sind Sie fertig, und der Bericht hat etwas anderes gemessen; prüfen Sie, ob die Funktion eingeschaltet ist.
  • variations left for you (Varianten für Sie): siehe unten. Das Skript fasst sie nie an.
  • held back (zurückgehalten): ein variables Produkt, dessen eigenen Wareneinsatz das Skript nicht übertragen hat, weil einige seiner Varianten im alten Plugin einen anderen haben. In WooCommerce verwendet eine Variante ohne eigenen Wert den des übergeordneten Produkts, das Übertragen gäbe diesen Varianten also unbemerkt dessen Zahl. Die Warnung nennt sie; stellen Sie diese zuerst ein und führen Sie das Skript dann erneut aus.
  • disagreements (Widersprüche): Dasselbe Produkt hat in den Feldern zweier Plugins verschiedene Kosten. Niemand kann sagen, welcher Wert aktuell ist, denn keiner trägt ein Datum. Das Skript gibt jeden Fall mit den Feldnamen aus, damit Sie ihn im Produkt-Editor klären, wo Sie sehen, was Sie wählen. Kosten werden als Zahlen verglichen, 12.50 in einem Feld und 12.5 in einem anderen sind also kein Widerspruch.
  • missing products (fehlende Produkte): Meta-Daten gelöschter Produkte. Harmlos; aufräumen sollte man sie nicht im selben Durchgang wie einen Schreibvorgang.

Eine Null in einem fremden Feld wird nicht übertragen, ein negativer Betrag ebenfalls nicht. In WooCommerce sind eine gespeicherte Null und ein fehlender Wareneinsatz dasselbe. Eine übertragene Null ändert also nichts außer der Zahl der Zeilen in Ihrer Datenbank; ein negativer Wareneinsatz ist ein Datenfehler, den man an der Quelle behebt, kein Wert zum Kopieren.

Varianten bleiben mit Absicht unangetastet

Eine Variante übernimmt den Wareneinsatz des übergeordneten Produkts, bis sie einen eigenen hat. Hat sie einen, entscheidet ein Schalter, ob dieser Wert den des übergeordneten Produkts ersetzt oder dazukommt. Bei einem Hemd mit einem Wareneinsatz von 24 kommt eine bestickte Variante mit 3 im additiven Modus auf 27 und im ersetzenden Modus auf 3. Das ist eine Entscheidung darüber, was die Zahl bedeutet, nicht darüber, wie hoch sie ist, und ein Skript kann sie aus den Daten nicht treffen: Das fremde Feld trägt einen Wert und keinen Modus.

Also: Lassen Sie das Skript die übergeordneten Produkte füllen, öffnen Sie dann die aufgelisteten Varianten und stellen Sie sie bewusst ein. Der eigene Wareneinsatz eines variablen Produkts wird nur übertragen, wenn keine seiner Varianten im alten Plugin etwas anderes sagt; sonst wird er wie oben zurückgehalten. Kosten bei variablen Produkten ist die Kurzfassung.

ATUM hat beim variablen Produkt selbst kein Feld für den Einkaufspreis, nur bei jeder seiner Varianten. In einem Shop mit ATUM und variablen Produkten erscheinen diese Zeilen deshalb unter „variations left for you“ und nicht unter „To move“. Das ist Vorsicht des Skripts, kein Fehler.

Der Probelauf, den Sie nicht überspringen sollten

  1. Eine Datenbanksicherung. Kein Produktexport, sondern eine Sicherung. Das Speichern eines Produkts berührt mehr als das eine Feld, um das es Ihnen geht.
  2. Den Probelauf Zeile für Zeile lesen. Vor allem die Widersprüche: Sie sind der einzige Teil, der einen Menschen braucht.
  3. Dann $DRY_RUN = false.
  4. Prüfen Sie im Shop, nicht im Terminal. Geben Sie eine Testbestellung für ein Produkt auf, das gerade einen Wareneinsatz bekommen hat, und prüfen Sie, ob die Bestellung ihn erfasst hat. Nur diese Prüfung deckt die ganze Kette ab und nicht nur das Produkt.

Nach dem Übertragen

Behalten Sie die Daten des alten Plugins. Löschen bringt nichts, und sie bleiben Ihr einziger Beleg dafür, wie die Kosten früher aussahen. Schalten Sie das alte Plugin später ab, bleiben seine Werte in der Datenbank; das Skript lässt sich erneut ausführen und meldet dann einfach, dass alles schon einen Wareneinsatz hat.

Führen Sie es nach einem Import erneut aus. Alles, was in das Feld des alten Plugins schreibt (eine Lieferantentabelle, eine Massenbearbeitung, ein Bestandsabgleich), legt Werte dorthin zurück, wo der Bericht sie nicht sieht. Das Skript erneut auszuführen ist sicher: Es überschreibt nie einen Wareneinsatz, der schon im Feld des Kerns steht.

Tragen Sie Kosten zusätzlich aus einer Tabelle ein, ist der Massenimport von Kosten die andere Hälfte derselben Arbeit.

Was das nicht behebt

  • Die Historie. Bestellungen vor der Übertragung bleiben für immer ohne Wareneinsatz. Warum nichts nachträglich ergänzt wird.
  • Erstattungen. Eine Erstattung lässt WooCommerce den Wareneinsatz jeder Position der ursprünglichen Bestellung aus dem heutigen Wareneinsatz neu berechnen. Frisch übertragene Werte können also genau die Historie neu schreiben, die sie beschreiben. Was dann passiert.
  • Echte Nullen. Ein Produkt, das wirklich nichts kostet, wird als gar kein Wareneinsatz gespeichert. Es bleibt auf der Liste fehlender Kosten, und nichts hier ändert das.
  • Variable Kosten. Versand, Zahlungsgebühren und Rücksendungen an den Lieferanten sind kein Wareneinsatz. Rohertrag ist kein Nettogewinn, und wir geben ihn auch nicht dafür aus.

Verhält sich Ihr Shop anders, als diese Seite beschreibt, lohnt es sich, uns zu schreiben: Diese Seiten werden korrigiert, wenn sie sich als falsch erweisen.