Merchgain
PL
Dostępne teraz
Następne, w tej kolejności
  • DeutschDE
  • EspañolES
  • FrançaisFR
  • ItalianoIT
  • PortuguêsPT
  • NederlandsNL

Język pojawia się tutaj wtedy, gdy jego strony są napisane. Nic na tej witrynie nie jest tłumaczone maszynowo.

Zainstaluj za darmoZainstaluj

Koszty już masz — tylko w niewłaściwym polu

Praca jest już wykonana. Wartości leżą w polu, o którym rdzeń WooCommerce nic nie wie, więc raport pokazuje lukę tam, gdzie masz dane. Ta strona je przenosi — po pokazaniu, co dokładnie by zrobiła.

Aktualizacja 2026-09-24

Jeśli ostrzeżenie o brakujących kosztach powiedziało Ci, że część produktów trzyma koszt w innej wtyczce — to jest ta strona, na którą wskazywało. Nic tu nie jest związane z Merchgainem: skrypt zapisuje do pola, które daje rdzeń WooCommerce, więc działa tak samo niezależnie od tego, czy zostaniesz przy naszej wtyczce.

Strona zakłada, że funkcja kosztu własnego sprzedaży jest już włączona. Jeśli nie — najpierw ją włącz. Przy wyłączonym przełączniku wartości zapisane do pola rdzenia nie trafią do żadnego zamówienia.

Przeniesienie kosztów naprawia przyszłość, nie przeszłość. WooCommerce zapisuje koszt na pozycji zamówienia w chwili sprzedaży, więc zamówienia sprzed przeniesienia zostaną bez kosztu na zawsze. Uzupełnienia wstecz nie ma i nie jest to przeoczenie. To jest powód, żeby zrobić to w tym tygodniu, a nie w przyszłym kwartale.

Które pola czyta skrypt

Gdzie leży kosztWtyczka, jej własną nazwą
_alg_wc_cog_costCost of Goods: Product Cost & Profit Calculator for WooCommerce (WPFactory)
_wc_cog_costWooCommerce Cost of Goods (SkyVerge, sprzedawana na woocommerce.com)
_wcj_purchase_priceBooster for WooCommerce — moduł 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
tabela atum_product_data, kolumna purchase_priceATUM Inventory Management for WooCommerce

Te miejsca zostały znalezione w kodzie tych wtyczek — w linii, w której każda z nich zapisuje wartość, a nie w stałej, etykiecie pola formularza ani liście zmian. To nie jest pedanteria i ta strona potknęła się na tym więcej niż raz. Wtyczka WPFactory ogłosiła wydanie z wpisem „prefix updated to wpfcogs”, który zmienił przedrostek funkcji i opcji, a nie klucza meta. ATUM nazywa swoje pole formularza _purchase_price, a od wersji 1.5.0 (grudzień 2018) trzyma wartość we własnej tabeli. Skrypt napisany z jednego albo drugiego szukałby pola, którego nie ma, i meldował czyste zero. Jedynym wyjątkiem jest WooCommerce Cost of Goods: to płatne rozszerzenie bez publicznego kodu, więc jego klucz pochodzi z jego własnej dokumentacji, potwierdzonej przez wtyczki, które z niego importują.

Sprostowanie, 24 września 2026. Do dziś ten skrypt czytał pole meta _purchase_price ATUM-a. ATUM nie zapisuje tam niczego od wersji 1.5.0: w sklepie, który używał ATUM-a wcześniej, te wiersze to kopie zamrożone w dniu, w którym ATUM przeniósł dane do własnej tabeli, a w nowszym sklepie nie ma ich wcale. Jeśli poprzednia wersja skryptu przeniosła coś w sklepie z ATUM-em, porównaj te koszty z cenami zakupu w ATUM-ie, zanim zaczniesz polegać na raporcie.

Dwie z tych wtyczek wymagają osobnego słowa. Booster potrafi doliczać do ceny zakupu „dodatkowe wydatki” i własne pola kosztów; skrypt przenosi samą cenę zakupu, bo wysyłka i podobne dodatki nie są kosztem towaru — jeśli je liczysz, dopisz je ręcznie. WooCommerce Cost of Goods od wersji 2.15.0 sam kopiuje swoje koszty do pola rdzenia, tak podaje jego lista zmian; w sklepie, który go używa, skrypt zamelduje głównie koszty, które już tam są.

Jeśli Twoje koszty leżą jeszcze gdzie indziej, kształt skryptu jest dobry — dopisz swój klucz na górze listy i sprawdź, co ta wartość znaczy, zanim uruchomisz: netto czy brutto, za sztukę czy za pozycję. Pole kosztu, w którym okazały się siedzieć ceny brutto, jest gorsze niż brak pola kosztu.

Skrypt

Czyta, decyduje i nie zapisuje niczego, dopóki nie zmienisz pierwszej linii. Zapisz jako przenies-koszty.php i uruchom przez WP-CLI.

<?php
/**
 * Przenosi koszty trzymane przez inna wtyczke do natywnego pola WooCommerce.
 * Uruchomienie:  wp eval-file przenies-koszty.php
 */

$NA_SUCHO = true;   // <- zmien na false dopiero po udanej probie

// Pola w postmeta: klucz meta => wtyczka.
$KLUCZE = 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',
);

// Wtyczki z wlasna tabela: tabela bez prefiksu => kolumna id, kolumna kosztu.
$TABELE = array(
    'atum_product_data' => array( 'product_id', 'purchase_price' ),   // ATUM Inventory Management for WooCommerce
);

global $wpdb;

$znalezione = array();

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

foreach ( $TABELE as $tabela => $kolumny ) {
    list( $kolumna_id, $kolumna_kosztu ) = $kolumny;
    $nazwa = $wpdb->prefix . $tabela;
    if ( $nazwa !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $nazwa ) ) ) ) {
        continue;                       // tej wtyczki nigdy tu nie bylo
    }
    $wiersze = $wpdb->get_results(
        "SELECT {$kolumna_id} AS id, {$kolumna_kosztu} AS koszt
           FROM {$nazwa}
          WHERE {$kolumna_kosztu} IS NOT NULL
            AND CAST( {$kolumna_kosztu} AS DECIMAL( 20, 6 ) ) > 0",
        ARRAY_A
    );
    foreach ( (array) $wiersze as $wiersz ) {
        $znalezione[ (int) $wiersz['id'] ][ "{$tabela}.{$kolumna_kosztu}" ] = $wiersz['koszt'];
    }
}

$przeniesione = 0;
$wlasne       = 0;
$warianty     = 0;
$wstrzymane   = 0;
$rozbieznosci = 0;
$znikniete    = 0;

foreach ( $znalezione as $id => $wartosci ) {
    $produkt = wc_get_product( $id );
    if ( ! $produkt ) {
        WP_CLI::warning( "Nie ma produktu o id {$id} - dane po czyms usunietym." );
        $znikniete++;
        continue;
    }

    // Koszt wariantu moze ZASTEPOWAC koszt rodzica albo sie do niego DODAWAC.
    // To decyzja o tym, co ta liczba znaczy, wiec skrypt jej nie podejmuje.
    if ( $produkt->is_type( 'variation' ) ) {
        WP_CLI::log( sprintf( '[wariant] %d %s: %s - zostawiam Tobie',
            $id, $produkt->get_name(), implode( ', ', $wartosci ) ) );
        $warianty++;
        continue;
    }

    $obecny = $produkt->get_cogs_value();
    if ( null !== $obecny && '' !== $obecny ) {
        $wlasne++;                      // nigdy nie nadpisujemy kosztu, ktory juz jest w polu rdzenia
        continue;
    }

    // Porownanie jako liczby: "12.50" w jednym polu i "12.5" w drugim to ten sam koszt.
    $liczby = array();
    foreach ( $wartosci as $wartosc ) {
        $liczby[] = (string) round( (float) $wartosc, 6 );
    }
    if ( count( array_unique( $liczby ) ) > 1 ) {
        $pokaz = array();
        foreach ( $wartosci as $klucz => $wartosc ) {
            $pokaz[] = "{$klucz}={$wartosc}";
        }
        WP_CLI::warning( sprintf( 'Dwa koszty nie zgadzaja sie przy %d %s: %s - pomijam',
            $id, $produkt->get_name(), implode( ', ', $pokaz ) ) );
        $rozbieznosci++;
        continue;
    }

    $koszt = (float) reset( $wartosci );

    // Koszt produktu zmiennego dziedziczy kazdy wariant, ktory nie ma wlasnego kosztu
    // w WooCommerce. Jesli ktorys ma w starej wtyczce INNY koszt - albo jawne zero
    // w tym samym polu - przeniesienie rodzica po cichu daloby mu liczbe rodzica.
    // Taki rodzic czeka, az warianty zostana ustawione.
    if ( $produkt->is_type( 'variable' ) ) {
        $inne = array();
        foreach ( $produkt->get_children() as $id_wariantu ) {
            $wariant = wc_get_product( $id_wariantu );
            if ( ! $wariant || null !== $wariant->get_cogs_value() ) {
                continue;               // wlasny koszt w WooCommerce nie jest dziedziczony
            }
            $jego = isset( $znalezione[ $id_wariantu ] ) ? $znalezione[ $id_wariantu ] : array();
            foreach ( array_keys( $wartosci ) as $klucz ) {
                $zero = isset( $KLUCZE[ $klucz ] ) ? get_post_meta( $id_wariantu, $klucz, true ) : '';
                if ( '' !== $zero && 0.0 === (float) $zero ) {
                    $jego[ $klucz ] = '0';
                }
            }
            foreach ( $jego as $wartosc ) {
                if ( (string) round( (float) $wartosc, 6 ) !== (string) round( $koszt, 6 ) ) {
                    $inne[] = $id_wariantu;
                    break;
                }
            }
        }
        if ( array() !== $inne ) {
            WP_CLI::warning( sprintf( 'Produkt zmienny %d %s: warianty %s maja w starej wtyczce inny koszt, a przeniesienie %s daloby im ten - wstrzymane, najpierw ustaw te warianty',
                $id, $produkt->get_name(), implode( ', ', $inne ), $koszt ) );
            $wstrzymane++;
            continue;
        }
    }

    if ( $NA_SUCHO ) {
        WP_CLI::log( sprintf( '[na sucho] %d %s: -> %s (z %s)',
            $id, $produkt->get_name(), $koszt, key( $wartosci ) ) );
    } else {
        $produkt->set_cogs_value( $koszt );
        $produkt->save();
        WP_CLI::log( sprintf( '%d %s = %s', $id, $produkt->get_name(), $koszt ) );
    }
    $przeniesione++;
}

WP_CLI::success( sprintf(
    '%s: %d | mialy juz koszt: %d | warianty zostawione: %d | wstrzymane: %d | rozbieznosci: %d | brak produktu: %d',
    $NA_SUCHO ? 'Do przeniesienia' : 'Przeniesione',
    $przeniesione, $wlasne, $warianty, $wstrzymane, $rozbieznosci, $znikniete
) );
wp eval-file przenies-koszty.php

Skrypt jest tu do przeczytania, a nie do pobrania jednym kliknięciem — celowo. Zapisuje do Twojej bazy, więc powinien przejść przez Twoje oczy, zanim przejdzie przez Twoje produkty.

Jak czytać próbę na sucho

Ostatnia linia jest całym raportem, a każda liczba na niej coś znaczy.

  • Do przeniesienia — produkty, które dostaną koszt. To jedyna liczba, która cokolwiek zmienia.
  • Miały już koszt — pole rdzenia jest wypełnione, więc skrypt trzyma ręce przy sobie. Jeśli ta liczba obejmuje cały katalog i nie ma czego przenosić, sprawdź, czy funkcja kosztu jest w ogóle włączona.
  • Warianty zostawione — niżej. Skrypt ich nie dotyka.
  • Wstrzymane — produkt zmienny, którego kosztu skrypt nie przeniósł, bo część jego wariantów ma w starej wtyczce inny koszt. W WooCommerce wariant bez własnego kosztu bierze koszt rodzica, więc przeniesienie rodzica po cichu dałoby tym wariantom jego liczbę. Ostrzeżenie wymienia je z numerów; najpierw je ustaw, potem uruchom skrypt jeszcze raz.
  • Rozbieżności — ten sam produkt ma różne koszty w polach dwóch wtyczek. Nikt nie orzeknie, który jest aktualny: żaden nie ma daty. Skrypt wypisuje każdy taki przypadek razem z nazwami pól, żeby dało się je rozstrzygnąć w edytorze produktu, patrząc na to, co się wybiera. Koszty są porównywane jako liczby, więc 12.50 w jednym polu i 12.5 w drugim to nie jest rozbieżność.
  • Brak produktu — meta po usuniętych produktach. Nieszkodliwe i nie warto tego sprzątać w tym samym przebiegu, co zapis.

Zera z cudzych pól nie są przenoszone, tak samo jak kwoty ujemne. W WooCommerce zapisane zero i brak kosztu to to samo, więc przeniesienie zera zmienia tylko liczbę wierszy w bazie; ujemny koszt to błąd danych do poprawienia u źródła, a nie wartość do kopiowania.

Warianty są zostawione celowo

Wariant dziedziczy koszt produktu nadrzędnego, dopóki nie ma własnego — a gdy go dostanie, jeden przełącznik decyduje, czy ten koszt zastępuje koszt rodzica, czy się do niego dodaje. Koszulka za 24 z wariantem „z haftem” za 3 daje 27 w trybie dodawania i 3 w trybie zastępowania. To jest decyzja o tym, co ta liczba znaczy, a nie czym jest — i skrypt nie wyczyta jej z danych, bo cudze pole niesie wartość, a nie tryb.

Czyli: pozwól skryptowi uzupełnić produkty nadrzędne, a potem otwórz wypisane warianty i ustaw je świadomie. Koszt samego produktu zmiennego przechodzi tylko wtedy, gdy żaden z jego wariantów nie mówi w starej wtyczce czego innego — inaczej jest wstrzymany, jak wyżej. Krótka wersja: koszty w wariantach.

ATUM nie ma pola ceny zakupu na samym produkcie wariantowym — tylko przy każdym z jego wariantów — więc sklep z ATUM-em i produktami wariantowymi zobaczy te wiersze właśnie na tej liście, a nie w kolumnie przeniesionych. To jest ostrożność skryptu, a nie jego porażka.

Próba, której nie warto pomijać

  1. Kopia bazy. Nie „eksport produktów” — kopia. Zapis produktu dotyka więcej niż tego jednego pola, o które tu chodzi.
  2. Próba na sucho, przeczytana linia po linii. Zwłaszcza rozbieżności: to jedyna część, która naprawdę wymaga człowieka.
  3. Dopiero potem $NA_SUCHO = false.
  4. Sprawdzenie w sklepie, nie w terminalu. Złóż jedno testowe zamówienie na produkt, który właśnie dostał koszt, i zobacz, czy zamówienie go zapisało. To jedyny test obejmujący cały łańcuch, a nie samo pole w produkcie.

Po przeniesieniu

Zostaw dane starej wtyczki. Nic nie zyskujesz na ich kasowaniu, a to Twój jedyny zapis tego, jakie koszty były wcześniej. Jeśli później wyłączysz starą wtyczkę, jej wartości zostaną w bazie; skrypt można uruchomić ponownie i po prostu zamelduje, że wszystko ma już koszt.

Uruchom go ponownie po każdym imporcie. Cokolwiek zapisuje do pola starej wtyczki — arkusz od dostawcy, edycja masowa, synchronizacja stanów — odkłada wartości tam, gdzie raport ich nie widzi. Ponowne uruchomienie jest bezpieczne: skrypt nigdy nie nadpisuje kosztu, który już jest w polu rdzenia.

Jeśli równolegle uzupełniasz koszty z arkusza, masowe uzupełnianie kosztów to druga połowa tej samej roboty.

Czego to nie naprawi

  • Historii. Zamówienia sprzed przeniesienia zostaną bez kosztu na zawsze. Dlaczego nie ma uzupełniania wstecz.
  • Zwrotów. Wystawienie zwrotu każe WooCommerce przeliczyć koszt każdej pozycji pierwotnego zamówienia z dzisiejszego kosztu produktu — świeżo przeniesione wartości mogą więc przepisać tę samą historię, którą opisują. Co się wtedy dzieje.
  • Prawdziwych zer. Produkt, który naprawdę nic nie kosztuje, jest zapisany jako brak kosztu, więc zostanie na liście braków i nic tu tego nie zmieni.
  • Kosztów zmiennych. Wysyłka, prowizje płatności i zwroty do dostawcy nie są kosztem własnym sprzedaży. Zysk brutto to nie zysk netto i nie udajemy, że jest inaczej.

Jeśli Twój sklep zachowuje się inaczej, niż opisuje ta strona, warto napisać — te strony poprawiamy, kiedy okazują się nieprawdziwe.