{"id":365278,"date":"2026-09-08T14:59:18","date_gmt":"2026-09-08T14:59:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/merchgain-profit-margin-reports-for-woocommerce\/"},"modified":"2026-10-11T20:15:48","modified_gmt":"2026-10-11T20:15:48","slug":"merchgain","status":"publish","type":"plugin","link":"https:\/\/frp.wordpress.org\/plugins\/merchgain\/","author":23551451,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.2.5","stable_tag":"0.2.5","tested":"7.1.3","requires":"6.7","requires_php":"7.4","requires_plugins":null,"header_name":"Merchgain - Gross Margin & Profit Reports for WooCommerce","header_author":"Baltano","header_description":"Profit and margin reports for WooCommerce, built on the native Cost of Goods Sold field. Reads your store, changes nothing.","assets_banners_color":"0f162a","last_updated":"2026-10-11 20:15:48","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/merchgain.com","header_author_uri":"https:\/\/baltano.com","rating":0,"author_block_rating":0,"active_installs":0,"downloads":374,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.2.0":{"tag":"0.2.0","author":"baltano","date":"2026-09-08 14:58:45","revision":3686880},"0.2.1":{"tag":"0.2.1","author":"baltano","date":"2026-09-18 14:58:06","revision":3702178},"0.2.2":{"tag":"0.2.2","author":"baltano","date":"2026-09-18 17:04:48","revision":3702377},"0.2.3":{"tag":"0.2.3","author":"baltano","date":"2026-09-23 11:41:53","revision":3709244},"0.2.4":{"tag":"0.2.4","author":"baltano","date":"2026-09-25 11:06:33","revision":3712920},"0.2.5":{"tag":"0.2.5","author":"baltano","date":"2026-10-11 20:15:48","revision":3740066}},"upgrade_notice":{"0.2.5":"<p>Orders placed before you entered costs now get a separate, clearly labelled estimate from today&#039;s product costs.<\/p>","0.2.4":"<p>The missing cost warning now finds ATUM purchase prices and recognises Booster and BjornTech PayPal POS.<\/p>","0.2.3":"<p>Fixes empty product and category tables and CSV export on MariaDB when sorted by profit (the default) or margin.<\/p>","0.2.2":"<p>The missing cost warning now tells you when your costs are kept by another plugin, and which one.<\/p>","0.2.1":"<p>New display name; faster category view with extensions installed; uninstall no longer touches extension data.<\/p>","0.2.0":"<p>First public release.<\/p>","0.1.1":"<p>The report now covers your whole order history instead of the last 12 months.<\/p>","0.1.0":"<p>First release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3686878,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3686878,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3686878,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3686878,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3686878,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3740066,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"Merchgain demo store\",\"description\":\"A small WooCommerce store with cost of goods and a year of orders, opened on the Merchgain profit and margin report.\",\"author\":\"baltano\",\"categories\":[\"WooCommerce\"]},\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=merchgain&range=365\",\"preferredVersions\":{\"php\":\"8.3\",\"wp\":\"latest\"},\"features\":{\"networking\":false},\"steps\":[{\"step\":\"login\",\"username\":\"admin\"},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"woocommerce\"},\"options\":{\"activate\":true}},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"merchgain\"},\"options\":{\"activate\":true}},{\"step\":\"setSiteOptions\",\"options\":{\"blogname\":\"Merchgain demo store\",\"woocommerce_feature_cost_of_goods_sold_enabled\":\"yes\",\"woocommerce_currency\":\"USD\",\"woocommerce_default_country\":\"US:CA\",\"woocommerce_coming_soon\":\"no\",\"woocommerce_analytics_enabled\":\"no\",\"woocommerce_show_marketplace_suggestions\":\"no\",\"woocommerce_allow_tracking\":\"no\"}},{\"step\":\"runPHP\",\"progress\":{\"caption\":\"Adding a year of demo orders\"},\"code\":\"<?php\\n\\\/**\\n * Sklep przykladowy dla podgladu Merchgaina w WordPress Playground.\\n *\\n * GDZIE TO DZIALA. Przycisk \\\"Preview\\\" na wordpress.org\\\/plugins\\\/merchgain\\n * otwiera playground.wordpress.net z naszym blueprintem: w SVN\\n * `assets\\\/blueprints\\\/blueprint.json`, w repo `assets-wporg\\\/blueprints\\\/`.\\n * `zbuduj.php` wkleja ten plik do kroku `runPHP`. W blueprincie go NIE\\n * poprawiac - poprawka idzie tutaj, potem `php playground\\\/zbuduj.php`.\\n *\\n * CO MA ZOBACZYC ODWIEDZAJACY. Raport za rok, ktory nowy uzytkownik dostaje\\n * dopiero po spelnieniu czterech warunkow z Plan\\\/11 par. 2 (WooCommerce 10.3+,\\n * wlaczony COGS, wpisane koszty, sprzedaz PO ich wpisaniu): karty, linia\\n * pokrycia, tabele produktow i kategorii, produkty bez kosztu z odnosnikami.\\n * Dane sa zwyczajne, z trzema rzeczami, ktore raport ma pokazac:\\n *  - zestaw kubkow sprzedawany PONIZEJ kosztu i komplet butelek na granicy\\n *    oplacalnosci - sortowanie po marzy wyciaga je na wierzch;\\n *  - trzy produkty bez kosztu do dzis - blok \\\"brak kosztu\\\" z odnosnikami;\\n *  - zamowienia z dwoch najstarszych miesiecy BEZ zapisanego kosztu: sklep\\n *    wpisal koszty pozniej, jak kazdy prawdziwy sklep po wlaczeniu COGS.\\n *    W 0.2.4 to linia pokrycia ponizej 100%, od 0.2.5 panel \\\"Szacunek\\\"\\n *    (Plan\\\/17). Uzgodnione z sesja 0.2.5 10.10.2026: na tych zamowieniach\\n *    zadnych zwrotow ani ponownego `calculate_totals()` - zwrot przelicza\\n *    `_cogs_value` calego zamowienia dzisiejszym kosztem (P2 z Plan\\\/05).\\n *\\n * NAZWY I WALUTA jak na zrzutach w katalogu (`tools\\\/scena-zrzutow.php`):\\n * po angielsku i w USD, bo listing jest angielski.\\n *\\n * DANE SA STALE. Nic nie jest losowane: produkty i ilosci w zamowieniach\\n * wynikaja z numeru zamowienia przez crc32. Daty licza sie od \\\"dzis\\\" w strefie\\n * sklepu, ale zadne zamowienie nie wypada dzis ani wczesniej niz 363 dni temu,\\n * wiec zakresy 7, 30, 90 i 365 dni daja te same liczby o kazdej godzinie.\\n *\\n * SZYBKOSC, bo odwiedzajacy czeka na ten krok przed pierwszym ekranem.\\n * Zmierzone w Playground CLI 3.1.57 (WC 11.2.1, SQLite) 10.10.2026:\\n *  - zamowienie zapisane ze zmiana statusu przez API: ~200 zapytan, 0,8 s\\n *    (notatka, zdjecie ze stanu, licznik sprzedazy, uprawnienia, maile);\\n *  - to samo jako `pending`: ~90 zapytan, 0,34 s; w jednej transakcji 0,09 s.\\n * Dlatego wszystko idzie w jednej transakcji, zamowienia zapisuja sie jako\\n * `pending`, a status docelowy ustawia jedno UPDATE na status (jak przy\\n * imporcie zamowien). Skrypt SPRAWDZA skutek publicznym API i gdy sie nie\\n * zgadza (np. inny magazyn zamowien), dokancza przez `update_status()`.\\n * Merchgain czyta status, date i pozycje - pozycje i ich koszt zapisuje rdzen.\\n * Bez pozycji wysylki i bez zwrotow: w raporcie ich nie widac, a kosztuja.\\n *\\n * TYLKO RAZ. Drugi przebieg nic nie dok\\u0142ada.\\n *\\n * @package Merchgain\\\\Playground\\n *\\\/\\n\\nrequire_once '\\\/wordpress\\\/wp-load.php';\\n\\nif ( ! function_exists( 'wc_get_product' ) || get_option( 'merchgain_demo_zasiew' ) ) {\\n\\treturn;\\n}\\n\\n$merchgain_demo = array(\\n\\t'stan'  => 'w toku',\\n\\t'czasy' => array(),\\n);\\n$merchgain_start = microtime( true );\\n$merchgain_znak  = $merchgain_start;\\n\\n\\\/**\\n * Czas etapu - sonda pokazuje, ktory etap ile trwal.\\n *\\\/\\n$merchgain_etap = static function ( string $nazwa ) use ( &$merchgain_demo, &$merchgain_znak ) {\\n\\t$teraz                             = microtime( true );\\n\\t$merchgain_demo['czasy'][ $nazwa ] = round( $teraz - $merchgain_znak, 2 );\\n\\t$merchgain_znak                    = $teraz;\\n};\\n\\n\\\/**\\n * Powtarzalna \\\"losowosc\\\": ten sam klucz zawsze daje te sama liczbe.\\n *\\\/\\n$merchgain_los = static function ( string $klucz, int $od, int $do ): int {\\n\\treturn $od + (int) ( crc32( 'merchgain-demo-' . $klucz ) % max( 1, $do - $od + 1 ) );\\n};\\n\\n\\\/\\\/ -------------------------------------------------------------------------\\n\\\/\\\/ 1. Sklep po pierwszym wejsciu do panelu.\\n\\\/\\\/ -------------------------------------------------------------------------\\n\\\/\\\/ WooCommerce konczy instalacje przy pierwszym `admin_init`: wlacza wtedy HPOS\\n\\\/\\\/ dla nowego sklepu, ale tylko gdy nie ma jeszcze zamowien. Ten krok idzie\\n\\\/\\\/ przed jakimkolwiek wejsciem do panelu, wiec bez tego zamowienia trafilyby\\n\\\/\\\/ do starego magazynu, a sklep zostalby na nim na stale (zmierzone:\\n\\\/\\\/ `woocommerce_custom_orders_table_enabled` = no). Wolamy to, co wolalby panel.\\nif ( 'yes' === get_option( 'woocommerce_newly_installed' ) && is_callable( array( 'WC_Install', 'newly_installed' ) ) ) {\\n\\tWC_Install::newly_installed();\\n}\\n\\\/\\\/ Bez tego pierwsze wejscie do panelu idzie na kreator WooCommerce.\\ndelete_transient( '_wc_activation_redirect' );\\nupdate_option( 'woocommerce_onboarding_profile', array( 'skipped' => true ) );\\n$merchgain_etap( 'pierwsze_wejscie' );\\n\\nif ( ! class_exists( '\\\\Automattic\\\\WooCommerce\\\\Utilities\\\\FeaturesUtil' )\\n\\t|| ! \\\\Automattic\\\\WooCommerce\\\\Utilities\\\\FeaturesUtil::feature_is_enabled( 'cost_of_goods_sold' ) ) {\\n\\t\\\/\\\/ Krok `setSiteOptions` wlacza COGS przed tym skryptem. Jesli mimo to jest\\n\\t\\\/\\\/ wylaczony, raport pokaze ekran powitalny - prawdziwy stan sklepu, wiec\\n\\t\\\/\\\/ nie zgadujemy, tylko zapisujemy powod dla sondy.\\n\\tupdate_option( 'merchgain_demo_zasiew', array( 'stan' => 'COGS wylaczony - brak danych' ), false );\\n\\treturn;\\n}\\n\\n\\\/\\\/ -------------------------------------------------------------------------\\n\\\/\\\/ 2. Katalog: osiem kategorii po trzy produkty.\\n\\\/\\\/ -------------------------------------------------------------------------\\n\\\/\\\/ [nazwa, kategorie, cena, koszt albo null, waga popularnosci, max sztuk]\\n$merchgain_katalog = array(\\n\\tarray( 'Leather weekender bag', array( 'Bags' ), 420.00, 252.00, 1, 1 ),\\n\\tarray( 'Canvas weekender bag', array( 'Bags' ), 165.00, 74.25, 2, 1 ),\\n\\tarray( 'Waxed canvas tote', array( 'Bags' ), 98.00, 39.20, 3, 2 ),\\n\\tarray( 'Heavyweight hoodie, premium', array( 'Hoodies' ), 89.00, 33.80, 5, 2 ),\\n\\tarray( 'Fleece-lined hoodie', array( 'Hoodies' ), 75.00, 30.75, 4, 2 ),\\n\\tarray( 'Zip hoodie, navy', array( 'Hoodies' ), 69.00, 31.05, 3, 2 ),\\n\\tarray( 'Premium T-shirt, black', array( 'T-shirts' ), 32.00, 9.40, 7, 3 ),\\n\\tarray( 'Heavyweight long-sleeve tee', array( 'T-shirts' ), 42.00, 15.50, 4, 2 ),\\n\\tarray( 'Ceramic mug 350 ml', array( 'Mugs' ), 16.00, 4.20, 8, 4 ),\\n\\tarray( 'Thermal mug 400 ml', array( 'Mugs' ), 29.00, 11.40, 4, 2 ),\\n\\t\\\/\\\/ Ponizej kosztu: popularny prezent, cena nie poszla za kosztem. W dwoch\\n\\t\\\/\\\/ kategoriach - ekran kategorii mowi wtedy, ze liczy go w obu.\\n\\tarray( 'Ceramic mug, set of six', array( 'Mugs', 'Gifts' ), 79.00, 83.40, 4, 1 ),\\n\\tarray( 'A2 poster, 250 g paper', array( 'Posters' ), 24.00, 5.10, 6, 3 ),\\n\\tarray( 'Framed print A3', array( 'Posters' ), 89.00, 47.00, 3, 1 ),\\n\\tarray( 'Limited art print, signed', array( 'Posters' ), 240.00, 96.00, 1, 1 ),\\n\\tarray( 'Sticker set, 12 pcs', array( 'Stickers' ), 9.00, 1.70, 8, 5 ),\\n\\tarray( 'Vinyl stickers A5', array( 'Stickers' ), 6.00, 0.90, 6, 5 ),\\n\\tarray( 'Transparent stickers A6', array( 'Stickers' ), 5.00, null, 5, 5 ),\\n\\tarray( 'Baseball cap, navy', array( 'Caps' ), 25.00, 8.20, 5, 2 ),\\n\\tarray( 'Embroidered varsity cap', array( 'Caps' ), 35.00, 13.60, 3, 1 ),\\n\\tarray( 'Merino wool beanie', array( 'Caps' ), 39.00, 16.80, 3, 2 ),\\n\\t\\\/\\\/ Na granicy oplacalnosci.\\n\\tarray( 'Insulated bottle set', array( 'Gifts' ), 59.00, 52.60, 3, 1 ),\\n\\tarray( 'Leather cardholder set', array( 'Gifts' ), 45.00, null, 2, 1 ),\\n\\tarray( 'Lambswool blanket', array( 'Gifts' ), 129.00, null, 1, 1 ),\\n);\\n\\n\\\/*\\n * Zamowien w miesiacu, od najnowszego: [0] to ostatnie 30 dni. Miesiace od\\n * numeru $merchgain_bez_kosztu_od to sprzedaz sprzed wpisania kosztow.\\n *\\\/\\n$merchgain_miesiace      = array( 9, 7, 6, 6, 7, 8, 6, 5, 5, 5, 6, 6 );\\n$merchgain_bez_kosztu_od = 10;\\n\\nglobal $wpdb;\\n$wpdb->query( 'START TRANSACTION' ); \\\/\\\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery\\n\\ntry {\\n\\twp_defer_term_counting( true );\\n\\n\\t$merchgain_kategorie = array();\\n\\tforeach ( array( 'Bags', 'Hoodies', 'T-shirts', 'Mugs', 'Posters', 'Stickers', 'Caps', 'Gifts' ) as $merchgain_nazwa ) {\\n\\t\\t$merchgain_term = term_exists( $merchgain_nazwa, 'product_cat' );\\n\\t\\tif ( ! $merchgain_term ) {\\n\\t\\t\\t$merchgain_term = wp_insert_term( $merchgain_nazwa, 'product_cat' );\\n\\t\\t}\\n\\t\\t$merchgain_kategorie[ $merchgain_nazwa ] = (int) $merchgain_term['term_id'];\\n\\t}\\n\\n\\t\\\/\\\/ Produkty powstaja BEZ kosztu - koszt dochodzi po pierwszych zamowieniach.\\n\\t$merchgain_produkty = array(); \\\/\\\/ [ produkt albo warianty, koszt, waga, max sztuk ]\\n\\tforeach ( $merchgain_katalog as $merchgain_i => $merchgain_p ) {\\n\\t\\t$merchgain_produkt = new WC_Product_Simple();\\n\\t\\t$merchgain_produkt->set_name( $merchgain_p[0] );\\n\\t\\t$merchgain_produkt->set_status( 'publish' );\\n\\t\\t$merchgain_produkt->set_sku( sprintf( 'DEMO-%02d', $merchgain_i + 1 ) );\\n\\t\\t$merchgain_produkt->set_regular_price( number_format( $merchgain_p[2], 2, '.', '' ) );\\n\\t\\t$merchgain_ids = array();\\n\\t\\tforeach ( $merchgain_p[1] as $merchgain_nazwa ) {\\n\\t\\t\\t$merchgain_ids[] = $merchgain_kategorie[ $merchgain_nazwa ];\\n\\t\\t}\\n\\t\\t$merchgain_produkt->set_category_ids( $merchgain_ids );\\n\\t\\t$merchgain_produkt->save();\\n\\t\\t$merchgain_produkty[] = array( $merchgain_produkt, $merchgain_p[3], $merchgain_p[4], $merchgain_p[5] );\\n\\t}\\n\\n\\t\\\/\\\/ Koszulka z rozmiarami: koszt na produkcie nadrzednym, warianty go\\n\\t\\\/\\\/ dziedzicza, a XL dok\\u0142ada swoj (flaga \\\"dodaj do kosztu rodzica\\\").\\n\\t$merchgain_koszulka = new WC_Product_Variable();\\n\\t$merchgain_koszulka->set_name( 'Unisex T-shirt, white' );\\n\\t$merchgain_koszulka->set_status( 'publish' );\\n\\t$merchgain_koszulka->set_sku( 'DEMO-TEE' );\\n\\t$merchgain_koszulka->set_category_ids( array( $merchgain_kategorie['T-shirts'] ) );\\n\\t$merchgain_atrybut = new WC_Product_Attribute();\\n\\t$merchgain_atrybut->set_name( 'Size' );\\n\\t$merchgain_atrybut->set_options( array( 'S', 'M', 'L', 'XL' ) );\\n\\t$merchgain_atrybut->set_visible( true );\\n\\t$merchgain_atrybut->set_variation( true );\\n\\t$merchgain_koszulka->set_attributes( array( $merchgain_atrybut ) );\\n\\t$merchgain_koszulka->save();\\n\\t$merchgain_warianty = array();\\n\\tforeach ( array( 'S', 'M', 'L', 'XL' ) as $merchgain_rozmiar ) {\\n\\t\\t$merchgain_wariant = new WC_Product_Variation();\\n\\t\\t$merchgain_wariant->set_parent_id( $merchgain_koszulka->get_id() );\\n\\t\\t$merchgain_wariant->set_attributes( array( 'size' => $merchgain_rozmiar ) );\\n\\t\\t$merchgain_wariant->set_regular_price( 'XL' === $merchgain_rozmiar ? '26.00' : '24.00' );\\n\\t\\t$merchgain_wariant->set_status( 'publish' );\\n\\t\\t$merchgain_wariant->save();\\n\\t\\t$merchgain_warianty[] = $merchgain_wariant;\\n\\t}\\n\\t$merchgain_produkty[] = array( $merchgain_warianty, null, 8, 3 );\\n\\twp_defer_term_counting( false );\\n\\n\\t\\\/\\\/ Pula z wagami: produkt o wadze 8 trafia sie 8 razy czesciej niz o wadze 1.\\n\\t$merchgain_pula = array();\\n\\tforeach ( $merchgain_produkty as $merchgain_i => $merchgain_p ) {\\n\\t\\tfor ( $merchgain_w = 0; $merchgain_w < $merchgain_p[2]; $merchgain_w++ ) {\\n\\t\\t\\t$merchgain_pula[] = $merchgain_i;\\n\\t\\t}\\n\\t}\\n\\t$merchgain_etap( 'katalog' );\\n\\n\\t$merchgain_klienci = array(\\n\\t\\tarray( 'Olivia', 'Bennett', 'CA' ), array( 'Liam', 'Carter', 'NY' ), array( 'Emma', 'Hughes', 'TX' ),\\n\\t\\tarray( 'Noah', 'Foster', 'WA' ), array( 'Ava', 'Morgan', 'IL' ), array( 'Lucas', 'Reed', 'OR' ),\\n\\t\\tarray( 'Mia', 'Turner', 'MA' ), array( 'Ethan', 'Brooks', 'CO' ), array( 'Chloe', 'Parker', 'CA' ),\\n\\t\\tarray( 'Mason', 'Ward', 'NY' ), array( 'Grace', 'Collins', 'TX' ), array( 'Logan', 'Price', 'WA' ),\\n\\t);\\n\\n\\t\\\/\\\/ Kalendarz: [dni temu, miesiac], dni rozlozone rowno w miesiacu.\\n\\t$merchgain_kalendarz = array();\\n\\tforeach ( $merchgain_miesiace as $merchgain_m => $merchgain_ile ) {\\n\\t\\tfor ( $merchgain_k = 0; $merchgain_k < $merchgain_ile; $merchgain_k++ ) {\\n\\t\\t\\t$merchgain_dni         = 1 + $merchgain_m * 30 + (int) floor( ( $merchgain_k + 0.5 ) * 30 \\\/ $merchgain_ile );\\n\\t\\t\\t$merchgain_kalendarz[] = array( min( 363, $merchgain_dni ), $merchgain_m );\\n\\t\\t}\\n\\t}\\n\\t\\\/\\\/ Od najstarszego: kolejnosc zapisu to kolejnosc numerow zamowien.\\n\\tusort(\\n\\t\\t$merchgain_kalendarz,\\n\\t\\tstatic function ( $a, $b ) {\\n\\t\\t\\treturn $b[0] <=> $a[0];\\n\\t\\t}\\n\\t);\\n\\n\\t$merchgain_teraz          = current_datetime();\\n\\t$merchgain_zamowienia     = array(); \\\/\\\/ id => status docelowy\\n\\t$merchgain_koszty_wpisane = false;\\n\\n\\tforeach ( $merchgain_kalendarz as $merchgain_nr => $merchgain_z ) {\\n\\t\\tlist( $merchgain_dni, $merchgain_m ) = $merchgain_z;\\n\\n\\t\\t\\\/\\\/ Koszty wpisane po dwoch najstarszych miesiacach. Wczesniejsze\\n\\t\\t\\\/\\\/ zamowienia zostaja bez `_cogs_value` - rdzen zapisuje koszt\\n\\t\\t\\\/\\\/ w chwili sprzedazy i nigdy go nie dopisuje wstecz.\\n\\t\\tif ( ! $merchgain_koszty_wpisane && $merchgain_m < $merchgain_bez_kosztu_od ) {\\n\\t\\t\\t$merchgain_etap( 'zamowienia_bez_kosztu' );\\n\\t\\t\\tforeach ( $merchgain_produkty as $merchgain_p ) {\\n\\t\\t\\t\\tif ( null !== $merchgain_p[1] ) {\\n\\t\\t\\t\\t\\t$merchgain_p[0]->set_cogs_value( $merchgain_p[1] );\\n\\t\\t\\t\\t\\t$merchgain_p[0]->save();\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t\\t$merchgain_koszulka->set_cogs_value( 7.80 );\\n\\t\\t\\t$merchgain_koszulka->save();\\n\\t\\t\\t$merchgain_warianty[3]->set_cogs_value( 1.20 );\\n\\t\\t\\t$merchgain_warianty[3]->set_cogs_value_is_additive( true );\\n\\t\\t\\t$merchgain_warianty[3]->save();\\n\\t\\t\\t$merchgain_koszty_wpisane = true;\\n\\t\\t\\t$merchgain_etap( 'koszty' );\\n\\t\\t}\\n\\n\\t\\t$merchgain_kiedy  = $merchgain_teraz->setTime(\\n\\t\\t\\t$merchgain_los( 'h' . $merchgain_nr, 8, 21 ),\\n\\t\\t\\t$merchgain_los( 'i' . $merchgain_nr, 0, 59 )\\n\\t\\t)->modify( '-' . $merchgain_dni . ' days' );\\n\\t\\t$merchgain_klient = $merchgain_klienci[ $merchgain_los( 'k' . $merchgain_nr, 0, count( $merchgain_klienci ) - 1 ) ];\\n\\n\\t\\t$merchgain_o = new WC_Order();\\n\\t\\t$merchgain_o->set_currency( get_woocommerce_currency() );\\n\\t\\t$merchgain_o->set_created_via( 'checkout' );\\n\\t\\t$merchgain_o->set_date_created( $merchgain_kiedy->getTimestamp() );\\n\\t\\t$merchgain_o->set_date_paid( $merchgain_kiedy->getTimestamp() );\\n\\t\\t$merchgain_o->set_billing_first_name( $merchgain_klient[0] );\\n\\t\\t$merchgain_o->set_billing_last_name( $merchgain_klient[1] );\\n\\t\\t$merchgain_o->set_billing_email( strtolower( $merchgain_klient[0] . '.' . $merchgain_klient[1] ) . '@example.com' );\\n\\t\\t$merchgain_o->set_billing_country( 'US' );\\n\\t\\t$merchgain_o->set_billing_state( $merchgain_klient[2] );\\n\\t\\t$merchgain_o->set_payment_method_title( 'Credit card' );\\n\\n\\t\\t$merchgain_juz   = array();\\n\\t\\t$merchgain_linii = $merchgain_los( 'l' . $merchgain_nr, 1, 3 );\\n\\t\\tfor ( $merchgain_l = 0; $merchgain_l < $merchgain_linii; $merchgain_l++ ) {\\n\\t\\t\\t$merchgain_i = $merchgain_pula[ $merchgain_los( 'p' . $merchgain_nr . '-' . $merchgain_l, 0, count( $merchgain_pula ) - 1 ) ];\\n\\t\\t\\tif ( isset( $merchgain_juz[ $merchgain_i ] ) ) {\\n\\t\\t\\t\\tcontinue;\\n\\t\\t\\t}\\n\\t\\t\\t$merchgain_juz[ $merchgain_i ] = true;\\n\\t\\t\\t$merchgain_p                   = $merchgain_produkty[ $merchgain_i ];\\n\\t\\t\\t$merchgain_o->add_product(\\n\\t\\t\\t\\tis_array( $merchgain_p[0] ) ? $merchgain_p[0][ $merchgain_los( 'r' . $merchgain_nr, 0, 3 ) ] : $merchgain_p[0],\\n\\t\\t\\t\\t$merchgain_los( 'q' . $merchgain_nr . '-' . $merchgain_l, 1, $merchgain_p[3] )\\n\\t\\t\\t);\\n\\t\\t}\\n\\n\\t\\t\\\/\\\/ Liczy sumy i zapisuje zamowienie z pozycjami; przy wlaczonym COGS\\n\\t\\t\\\/\\\/ rdzen zapisuje przy tym koszt kazdej pozycji (dzisiejszy koszt x ilosc).\\n\\t\\t$merchgain_o->calculate_totals( false );\\n\\n\\t\\t\\\/\\\/ Ostatnie dni w realizacji, co 29. anulowane (raport ich nie liczy).\\n\\t\\tif ( $merchgain_dni <= 3 ) {\\n\\t\\t\\t$merchgain_status = 'wc-processing';\\n\\t\\t} elseif ( 0 === $merchgain_nr % 29 ) {\\n\\t\\t\\t$merchgain_status = 'wc-cancelled';\\n\\t\\t} else {\\n\\t\\t\\t$merchgain_status = 'wc-completed';\\n\\t\\t}\\n\\t\\t$merchgain_zamowienia[ $merchgain_o->get_id() ] = $merchgain_status;\\n\\t}\\n\\t$merchgain_etap( 'zamowienia' );\\n\\n\\t\\\/\\\/ -------------------------------------------------------------------------\\n\\t\\\/\\\/ 3. Statusy - jedno UPDATE na status, potem sprawdzenie skutku.\\n\\t\\\/\\\/ -------------------------------------------------------------------------\\n\\t$merchgain_hpos    = class_exists( '\\\\Automattic\\\\WooCommerce\\\\Utilities\\\\OrderUtil' )\\n\\t\\t&& \\\\Automattic\\\\WooCommerce\\\\Utilities\\\\OrderUtil::custom_orders_table_usage_is_enabled();\\n\\t$merchgain_statusy = array_count_values( $merchgain_zamowienia );\\n\\tforeach ( array_keys( $merchgain_statusy ) as $merchgain_status ) {\\n\\t\\t$merchgain_id = implode( ',', array_map( 'intval', array_keys( $merchgain_zamowienia, $merchgain_status, true ) ) );\\n\\t\\tif ( $merchgain_hpos ) {\\n\\t\\t\\t$wpdb->query( \\\"UPDATE {$wpdb->prefix}wc_orders SET status = '{$merchgain_status}' WHERE id IN ({$merchgain_id})\\\" ); \\\/\\\/ phpcs:ignore WordPress.DB\\n\\t\\t}\\n\\t\\tif ( ! $merchgain_hpos || 'yes' === get_option( 'woocommerce_custom_orders_table_data_sync_enabled' ) ) {\\n\\t\\t\\t$wpdb->query( \\\"UPDATE {$wpdb->posts} SET post_status = '{$merchgain_status}' WHERE ID IN ({$merchgain_id})\\\" ); \\\/\\\/ phpcs:ignore WordPress.DB\\n\\t\\t}\\n\\t}\\n\\twp_cache_flush();\\n\\n\\t\\\/\\\/ Skutek liczony publicznym API, nie liczba wykonanych UPDATE-ow.\\n\\t$merchgain_poprawki = 0;\\n\\tforeach ( $merchgain_statusy as $merchgain_status => $merchgain_ile ) {\\n\\t\\t$merchgain_sa = wc_get_orders(\\n\\t\\t\\tarray(\\n\\t\\t\\t\\t'status' => $merchgain_status,\\n\\t\\t\\t\\t'type'   => 'shop_order',\\n\\t\\t\\t\\t'limit'  => -1,\\n\\t\\t\\t\\t'return' => 'ids',\\n\\t\\t\\t)\\n\\t\\t);\\n\\t\\tif ( count( $merchgain_sa ) === $merchgain_ile ) {\\n\\t\\t\\tcontinue;\\n\\t\\t}\\n\\t\\tforeach ( array_keys( $merchgain_zamowienia, $merchgain_status, true ) as $merchgain_id ) {\\n\\t\\t\\t$merchgain_o = wc_get_order( $merchgain_id );\\n\\t\\t\\tif ( $merchgain_o && 'wc-' . $merchgain_o->get_status() !== $merchgain_status ) {\\n\\t\\t\\t\\t$merchgain_o->update_status( substr( $merchgain_status, 3 ) );\\n\\t\\t\\t\\t$merchgain_poprawki++;\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\\t$merchgain_etap( 'statusy' );\\n\\n\\t\\\/\\\/ Prosba o recenzje (od 0.2.3) pyta o zdanie o wtyczce na WLASNYM sklepie;\\n\\t\\\/\\\/ odwiedzajacy podglad takiego zdania jeszcze nie ma.\\n\\tupdate_user_meta( 1, 'merchgain_review_asked', 'dismissed' );\\n\\n\\t$wpdb->query( 'COMMIT' ); \\\/\\\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery\\n\\n\\t$merchgain_demo['stan']     = 'gotowe';\\n\\t$merchgain_demo['zamowien'] = count( $merchgain_zamowienia );\\n\\t$merchgain_demo['statusy']  = $merchgain_statusy;\\n\\t$merchgain_demo['poprawki'] = $merchgain_poprawki;\\n\\t$merchgain_demo['hpos']     = $merchgain_hpos;\\n} catch ( \\\\Throwable $merchgain_blad ) {\\n\\t$wpdb->query( 'ROLLBACK' ); \\\/\\\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery\\n\\t$merchgain_demo['stan'] = 'blad: ' . $merchgain_blad->getMessage() . ' @ ' . basename( $merchgain_blad->getFile() ) . ':' . $merchgain_blad->getLine();\\n}\\n\\n$merchgain_demo['sekund'] = round( microtime( true ) - $merchgain_start, 2 );\\nupdate_option( 'merchgain_demo_zasiew', $merchgain_demo, false );\\n\"}]}"}},"all_blocks":[],"tagged_versions":["0.2.0","0.2.1","0.2.2","0.2.3","0.2.4","0.2.5"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3740064,"resolution":"1","location":"assets","locale":"","width":1280,"height":962},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3740064,"resolution":"2","location":"assets","locale":"","width":1280,"height":535},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3740064,"resolution":"3","location":"assets","locale":"","width":1280,"height":494},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3740064,"resolution":"4","location":"assets","locale":"","width":1280,"height":521}},"screenshots":{"1":"The report for the period you pick: gross profit, revenue, cost of goods and margin. The bar and\nthe line under the cards show how much of that revenue has a cost recorded at all. The panel\nbelow estimates the items sold before their cost was recorded, from today's product costs, and\nnever adds the estimate to the cards above. Products that still have no cost are listed by name,\nwith links straight to them.","2":"Profit product by product - items sold, revenue, cost, profit and margin, with a bar beside\nevery margin, sorted by whichever column you pick. Items sold before their cost was recorded get\nestimated profit and margin columns of their own, kept apart from the recorded figures.","3":"The same sales grouped by product category, with the estimate in columns of its own. A product\nthat sits in several categories is counted in each of them, so the totals can add up to more\nthan the summary - and the screen says so instead of hiding it.","4":"What you see before the Cost of Goods Sold feature is switched on: the three steps to turn it\non, and the warning that orders placed while it is off record no cost at all - a gap that cannot\nbe filled in later."}},"plugin_section":[],"plugin_tags":[233971,247065,281460,281461,188496],"plugin_category":[],"plugin_contributors":[278694],"plugin_business_model":[],"class_list":["post-365278","plugin","type-plugin","status-publish","hentry","plugin_tags-cogs","plugin_tags-cost-of-goods-sold","plugin_tags-gross-margin","plugin_tags-gross-profit","plugin_tags-profit-report","plugin_contributors-baltano","plugin_committers-baltano"],"banners":{"banner":"https:\/\/ps.w.org\/merchgain\/assets\/banner-772x250.png?rev=3686878","banner_2x":"https:\/\/ps.w.org\/merchgain\/assets\/banner-1544x500.png?rev=3686878","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/merchgain\/assets\/icon.svg?rev=3686878","icon":"https:\/\/ps.w.org\/merchgain\/assets\/icon.svg?rev=3686878","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/merchgain\/assets\/screenshot-1.png?rev=3740064","caption":"The report for the period you pick: gross profit, revenue, cost of goods and margin. The bar and\nthe line under the cards show how much of that revenue has a cost recorded at all. The panel\nbelow estimates the items sold before their cost was recorded, from today's product costs, and\nnever adds the estimate to the cards above. Products that still have no cost are listed by name,\nwith links straight to them."},{"src":"https:\/\/ps.w.org\/merchgain\/assets\/screenshot-2.png?rev=3740064","caption":"Profit product by product - items sold, revenue, cost, profit and margin, with a bar beside\nevery margin, sorted by whichever column you pick. Items sold before their cost was recorded get\nestimated profit and margin columns of their own, kept apart from the recorded figures."},{"src":"https:\/\/ps.w.org\/merchgain\/assets\/screenshot-3.png?rev=3740064","caption":"The same sales grouped by product category, with the estimate in columns of its own. A product\nthat sits in several categories is counted in each of them, so the totals can add up to more\nthan the summary - and the screen says so instead of hiding it."},{"src":"https:\/\/ps.w.org\/merchgain\/assets\/screenshot-4.png?rev=3740064","caption":"What you see before the Cost of Goods Sold feature is switched on: the three steps to turn it\non, and the warning that orders placed while it is off record no cost at all - a gap that cannot\nbe filled in later."}],"raw_content":"<!--section=description-->\n<p>WooCommerce tells you what you sold. It does not tell you what you earned.<\/p>\n\n<p>Since WooCommerce 10.3 the cost of goods sold lives in the core product editor, and every order\nrecords the cost at the moment of sale. Merchgain reads that field and answers the question the\nsales report cannot: how much of this revenue is actually yours. It is a WooCommerce profit report\nbuilt on data your store already has - no import, no second place to keep costs.<\/p>\n\n<p>It only reads. No product, no price, no order, no cost value is ever written back to your store.<\/p>\n\n<h4>What you get<\/h4>\n\n<ul>\n<li><strong>Summary cards<\/strong> - revenue, cost of goods, gross profit and margin for the period you pick.<\/li>\n<li><strong>Profit per product<\/strong> - sortable, searchable, paginated.<\/li>\n<li><strong>Profit per category<\/strong> - the same sales grouped by product category. A product that sits\nin several categories counts in each of them, so category totals can add up to more than\nthe summary - the screen and the CSV file both say so.<\/li>\n<li><strong>Missing cost warning<\/strong> - how many products have no cost recorded, with links straight to\nthe products, and how much of your revenue that leaves uncovered. If the cost is kept by\nanother plugin instead, it says so and names it, so an empty report stops being a mystery.<\/li>\n<li><strong>Estimate for older orders<\/strong> - items sold before you entered costs have none recorded, so the\nreport estimates them from each product's cost as it is today. The estimate sits in a panel and\ncolumns of its own, marked as an estimate, and is never added to the recorded figures - enter\nyour costs today and see an estimate for last year straight away, not just a row of dashes.<\/li>\n<li><strong>CSV export<\/strong> - the current view, exactly as you see it on screen. Very large reports are\ncapped at 20,000 rows and the file says so on its last line, so a short file is never a\nsilent one - and a category export that adds up to more than the summary says why.<\/li>\n<li><strong>Foreign currency warning<\/strong> - orders in a currency other than your store currency are named\nand left out, instead of being quietly added into one wrong total.<\/li>\n<\/ul>\n\n<h4>What makes it different<\/h4>\n\n<ul>\n<li><strong>CSV export in the free version.<\/strong> Not behind an upgrade.<\/li>\n<li><strong>The missing cost warning is a feature, not an upsell.<\/strong> You see coverage next to every\nnumber, so you always know how much of the report rests on real data.<\/li>\n<li><strong>Works without WooCommerce Analytics.<\/strong> Merchgain runs its own queries against the order\ntables, so it does not require the Analytics module to be enabled.<\/li>\n<li><strong>Nothing leaves your server.<\/strong> No account, no external service, no telemetry, no calls home.\nThe report is calculated on your hosting and stays there.<\/li>\n<\/ul>\n\n<h4>How the numbers are calculated<\/h4>\n\n<p>This is a cost of goods sold report, so profit and margin rest on the cost recorded at the moment\nof sale - not on a current price list. The one exception is the estimate for items sold without a\nrecorded cost: it uses today's product cost, says so wherever it appears, and is never added to the\nrecorded figures.<\/p>\n\n<p>Revenue is the net line total of sold items after discounts, excluding tax and shipping. Refunds\ncount as negative amounts in the period they were issued, not in the period of the original sale.\nOrders counted by default: completed, processing and refunded.<\/p>\n\n<p>Margin is profit divided by revenue - not markup on cost. A product bought for 60 and sold for\n100 has a 40% margin here. That single distinction is what separates a profit margin report from\na sales report.<\/p>\n\n<p>Profit and margin are calculated only on items whose cost is known. An item with no cost recorded\nstill counts towards revenue, but it is never treated as if it had cost zero - the report says so\ninstead of quietly inflating your margin.<\/p>\n\n<p>Documentation and answers: https:\/\/merchgain.com\/<\/p>\n\n<p>From the makers of Merchlint.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install and activate the plugin. WooCommerce 10.3 or newer is required.<\/li>\n<li>Go to WooCommerce -&gt; Settings -&gt; Advanced -&gt; Features and switch on \"Cost of Goods Sold\".<\/li>\n<li>Fill in the cost on your products - the field is in the General tab of the product editor.<\/li>\n<li>Open \"Profit &amp; margin\" in the admin menu - its own item, just below the WooCommerce block.<\/li>\n<\/ol>\n\n<p>New orders record the product cost at the moment of sale. Orders placed before your costs were in\nplace have none recorded, so the report estimates them from today's product costs - separately\nfrom the recorded figures.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20is%20margin%20calculated%3F\"><h3>How is margin calculated?<\/h3><\/dt>\n<dd><p>Profit divided by revenue. A product bought for 60 and sold for 100 gives 40 profit and a 40%\nmargin. This is not markup on cost, which for the same product would be 66.7%. The two get mixed\nup often, so the definition is printed under the report as well.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/MARGIN\/<\/p><\/dd>\n<dt id=\"where%20do%20the%20costs%20come%20from%3F\"><h3>Where do the costs come from?<\/h3><\/dt>\n<dd><p>From the Cost of Goods Sold field built into WooCommerce 10.3 and newer. Merchgain does not store\ncosts of its own and does not add a second cost field to your products.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/COGS\/<\/p><\/dd>\n<dt id=\"does%20any%20data%20leave%20my%20server%3F\"><h3>Does any data leave my server?<\/h3><\/dt>\n<dd><p>No. Merchgain makes no outgoing connections at all - no analytics, no licence check, no update\nping of its own. Everything is calculated by your own database and displayed in your own admin.<\/p><\/dd>\n<dt id=\"what%20about%20orders%20placed%20before%20i%20filled%20in%20the%20costs%3F\"><h3>What about orders placed before I filled in the costs?<\/h3><\/dt>\n<dd><p>They have no cost recorded, and Merchgain reports them as \"no cost data\" rather than as a cost of\nzero. Their revenue still counts; they are left out of profit and margin, and the coverage line\ntells you how much of your revenue that is.<\/p>\n\n<p>Once their products have a cost, the report also estimates them: today's product cost times the\nquantity sold, the same way WooCommerce would record it if the sale happened today. The estimate\nhas a panel and columns of its own, every estimated figure is marked, and it is never added to the\nrecorded profit. If a cost price changed since the sale, the estimate is off by exactly that\nchange - which is why it stays apart.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/EST\/<\/p><\/dd>\n<dt id=\"my%20product%20genuinely%20costs%20zero%20and%20it%20shows%20up%20as%20missing%20a%20cost.%20why%3F\"><h3>My product genuinely costs zero and it shows up as missing a cost. Why?<\/h3><\/dt>\n<dd><p>Because WooCommerce cannot tell the two apart. Saving a cost of zero - on the product or on an\norder item - makes the core store nothing at all rather than a zero, so \"cost of zero\" and \"no\ncost entered\" end up identical in the database. Rather than guess, Merchgain always says \"no cost\ndata\" and never \"cost 0\". Free samples, giveaways and downloadable extras will therefore appear on\nthe missing cost list.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/ZERO\/<\/p><\/dd>\n<dt id=\"i%20issued%20a%20refund%20and%20my%20historical%20profit%20changed.%20why%3F\"><h3>I issued a refund and my historical profit changed. Why?<\/h3><\/dt>\n<dd><p>That is WooCommerce, not Merchgain. Creating a refund makes the core recalculate the cost of every\nitem on the parent order using the product's cost <em>as it is today<\/em>. If the product cost changed in\nthe meantime, the stored history changes with it. Merchgain reports what is in the database; it\ndoes not silently keep a second copy.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/REWRITE\/<\/p><\/dd>\n<dt id=\"which%20month%20does%20a%20refund%20belong%20to%3F\"><h3>Which month does a refund belong to?<\/h3><\/dt>\n<dd><p>The month the refund was issued, not the month of the original sale. A September sale refunded in\nNovember leaves September untouched and shows as a negative amount in November. Reports of closed\nmonths do not change behind your back.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/REFUND\/<\/p><\/dd>\n<dt id=\"i%20have%20orders%20in%20euros%20and%20dollars.%20what%20happens%20to%20them%3F\"><h3>I have orders in euros and dollars. What happens to them?<\/h3><\/dt>\n<dd><p>They are left out of the report and named in a warning above the numbers. Merchgain does not\nconvert exchange rates, and adding several currencies into one total would produce a number that\nlooks right and is not.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/CURR\/<\/p><\/dd>\n<dt id=\"i%20keep%20my%20costs%20in%20another%20plugin.%20why%20does%20merchgain%20show%20them%20as%20missing%3F\"><h3>I keep my costs in another plugin. Why does Merchgain show them as missing?<\/h3><\/dt>\n<dd><p>Because those costs live in that plugin's own storage - a field of its own or, for ATUM, a table\nof its own - not in the WooCommerce core field. Merchgain tells you so instead of leaving you\nguessing: the missing cost warning counts how many of those products have a cost kept by another\nplugin and names it. Recognised: Cost of Goods: Product Cost &amp; Profit Calculator for WooCommerce,\nWooCommerce Cost of Goods, ATUM Inventory Management for WooCommerce, Booster for WooCommerce,\nBjornTech PayPal POS integration for WooCommerce, Ni Cost of Goods for WooCommerce and Cost Of\nGoods For WooCommerce.<\/p>\n\n<p>It counts them; it never uses them. A number taken from a field nobody can check is worse than a\ngap - and a shop with two cost plugins in its history has two fields that disagree, neither of\nthem dated. Moving the values into the WooCommerce field is the durable answer, and the page\nbelow shows how.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/OTHER\/<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20high-performance%20order%20storage%3F\"><h3>Does it work with High-Performance Order Storage?<\/h3><\/dt>\n<dd><p>Yes, and with the classic post-based storage as well. Cost values live in the order items table,\nwhich is the same in both layouts.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/HPOS\/<\/p><\/dd>\n<dt id=\"which%20orders%20are%20counted%3F\"><h3>Which orders are counted?<\/h3><\/dt>\n<dd><p>Completed, processing and refunded. Refunded is in the list on purpose: a full refund flips the\nparent order to that status, and leaving it out would make a fully refunded order count as minus\nits own value instead of zero. WooCommerce Analytics excludes only pending, failed and cancelled\nby default, so this set is one status narrower - it skips on-hold.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/STAT\/<\/p><\/dd>\n<dt id=\"my%20category%20totals%20add%20up%20to%20more%20than%20the%20summary%20card.%20why%3F\"><h3>My category totals add up to more than the summary card. Why?<\/h3><\/dt>\n<dd><p>Because a product that sits in several categories is counted in each of them. A mug in both\n\"Kitchen\" and \"Gifts\" adds its sales to both rows, so the rows add up to more than the store\nactually sold. The alternative - picking one category and dropping the sale from the other -\nwould quietly understate every shared category, which is worse. The summary cards are counted\nfrom the order items themselves, so they are never double counted. The screen says this under\nthe table, and a CSV export of the category view says it on the last line, with both numbers.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/CAT\/<\/p><\/dd>\n<dt id=\"how%20much%20history%20does%20the%20report%20cover%3F\"><h3>How much history does the report cover?<\/h3><\/dt>\n<dd><p>All of it. There is no date limit. Any range can be selected, including a single month from\nyears back; profit and margin appear wherever a cost was recorded on the sold item.<\/p><\/dd>\n<dt id=\"does%20it%20slow%20my%20store%20down%3F\"><h3>Does it slow my store down?<\/h3><\/dt>\n<dd><p>The report runs in the admin only, never on the storefront, and results are cached for an hour.\nEntering a cost in the product editor clears that cache immediately, so the numbers never wait for\na timer. For anything else that changes the figures - an order status, a refund issued elsewhere,\nan import - there is a \"Recalculate\" button at the top of the report.<\/p><\/dd>\n<dt id=\"who%20can%20see%20the%20report%3F\"><h3>Who can see the report?<\/h3><\/dt>\n<dd><p>Anyone with the \"view WooCommerce reports\" capability, which is the same permission WooCommerce\nuses for its own sales reports. A shop manager has it, and so does a bookkeeper account limited to\nreports only. The report has its own item in the admin menu, \"Profit &amp; margin\", sitting just below\nthe WooCommerce block - not inside the WooCommerce submenu, where an account limited to reports\nwould never see it: that submenu needs a permission such an account does not have.<\/p>\n\n<p>Longer answer: https:\/\/merchgain.com\/r\/PERM\/<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>0.2.5<\/h4>\n\n<ul>\n<li>New: an estimate for items sold without a recorded cost. WooCommerce records the cost on the\norder at the moment of sale, so a shop that enters its costs today has none on last year's\norders. The report now estimates those items from each product's cost as it is today - the\nsame rule WooCommerce uses when it records a cost, variations included - and shows the result\nin a panel of its own under the summary cards.<\/li>\n<li>The estimate is never added to the recorded figures. The summary cards, the Cost, Profit and\nMargin columns and the coverage line are calculated exactly as before; the estimate has its own\ncards, its own \"Estimated profit\" and \"Estimated margin\" columns, which can be sorted, and an\napproximately-equal sign on every estimated figure on screen.<\/li>\n<li>The CSV export has five new columns at the end - revenue, cost, profit, margin and item count of\nthe estimate - and explains them in a note at the end of the file. Existing columns keep their\nplaces.<\/li>\n<li>Items with no cost even today - a product with no cost or a cost of zero, or one deleted since\nthe sale - are not estimated; when the panel is shown, it says how much revenue that leaves out.<\/li>\n<li>Nothing is written to the store: the estimate is calculated when the report is opened.<\/li>\n<li>The one-time review request can now also appear below a report that has only an estimate.<\/li>\n<li>New look: gross profit is the largest number on the screen, with revenue, cost of goods and\nmargin beside it in a summary panel, and a bar shows how revenue splits into cost, profit and\nthe part with no cost recorded. Margins in the table have a bar beside them, the search box and\nthe CSV button sit next to the table they work on, and the screen uses the colours of the\nMerchgain icon. The numbers and how they are calculated are unchanged.<\/li>\n<li>Fixed: the link next to the coverage line always said 74%, whatever the real share was; it now\nquotes the line. A share just under 100% or just over 0% shows one decimal, such as 99.6%,\ninstead of being rounded to 100% or 0%.<\/li>\n<li>Fixed: switching from a product search to the category view kept the search phrase, so the\nscreen said the search filtered a table it did not filter, and the category export lost its\nnote about products counted in several categories.<\/li>\n<li>Fixed: an account that can see reports but not edit products got the names in the missing cost\nlist as empty links that reloaded the report. They are plain names now.<\/li>\n<\/ul>\n\n<h4>0.2.4<\/h4>\n\n<ul>\n<li>Fixed: the missing cost warning did not see the purchase prices kept by ATUM Inventory\nManagement. Since its version 1.5.0 ATUM keeps them in a table of its own, while Merchgain was\nreading an old product field that ATUM no longer writes to. It reads the table now. The old\nfield is no longer counted at all: in a shop that ran ATUM before 1.5.0 it holds a frozen copy,\nand other plugins use a field of the same name, so no single plugin could honestly be named\nfor it.<\/li>\n<li>Fixed: costs kept by WooCommerce Cost of Goods were credited to the WPFactory cost plugin, which\nonly imports them. The warning now names the right plugin.<\/li>\n<li>Fixed: a variation with no cost of its own in Cost of Goods: Product Cost &amp; Profit Calculator for\nWooCommerce takes its parent's cost there, and is now counted, so a shop that keeps those costs\non variable products no longer sees a zero.<\/li>\n<li>The warning now also recognises Booster for WooCommerce and BjornTech PayPal POS integration\nfor WooCommerce, and it names each plugin by the name the plugin gives itself.<\/li>\n<li>When Booster for WooCommerce is set to \"Treat Variable Products as Simple Products\", a variation\ntakes its parent's purchase price there, and the warning counts it as a cost kept in Booster.<\/li>\n<li>Negative amounts are no longer counted either - only amounts above zero.<\/li>\n<\/ul>\n\n<h4>0.2.3<\/h4>\n\n<ul>\n<li>Fixed: on MariaDB the product and category tables - and their CSV export - came out empty when\nsorted by profit or by margin, and profit is the default sort. The database rejected the sort\nexpression, and the screen showed no rows and no error. MySQL was not affected. No figure was ever\nwrong: the summary cards and the other sorts worked.<\/li>\n<li>Below a report with at least one costed sale, a one-time request to review the plugin on\nWordPress.org, whatever your verdict. It shows only on the report screen, never on the dashboard,\nand stays closed for good once you follow it or choose \"Don't ask again\". The choice is kept per\nuser and removed on uninstall.<\/li>\n<li>New screenshots, taken on an English demo store, with captions that say what each screen shows.<\/li>\n<\/ul>\n\n<h4>0.2.2<\/h4>\n\n<ul>\n<li>The missing cost warning now says when a cost is kept by another plugin. It counts those\nproducts and names the plugin: Cost of Goods for WooCommerce (both of its fields), ATUM\nInventory Management, NI WooCommerce Cost of Goods and Cost of Goods. A cost of zero in such a\nfield is not counted, because moving a zero changes nothing.<\/li>\n<li>Those values are counted, never used. Profit and margin still come only from the cost field\nWooCommerce itself provides - a figure taken from a field nobody can verify would be worse than\nthe gap it fills.<\/li>\n<\/ul>\n\n<h4>0.2.1<\/h4>\n\n<ul>\n<li>The plugin is now listed as \"Merchgain - Gross Margin &amp; Profit Reports for WooCommerce\". Only the\ndisplayed name changed: same plugin, same directory slug, same settings, same data.<\/li>\n<li>Faster category view when an extension adds order-level costs to the report: the grouping used\nto run two queries per product. On a store with 11,000 sold items that was 6,978 queries and\n2.6 seconds; it is now 18 queries and 0.8 seconds. The figures are unchanged.<\/li>\n<li>Uninstalling no longer removes cached data belonging to Merchgain extensions. Each extension\ncleans up after itself.<\/li>\n<\/ul>\n\n<h4>0.2.0<\/h4>\n\n<ul>\n<li>First public release. Versions 0.1.0 and 0.1.1 were review builds submitted to the plugin\ndirectory and were never published, so this is the first version anyone can install. The code\nis the one that passed review; only the version number changed.<\/li>\n<\/ul>\n\n<h4>0.1.1<\/h4>\n\n<ul>\n<li>The report no longer limits how far back it reaches. The 365-day window is gone from the\ncode entirely, along with the notice that explained it.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<ul>\n<li>First working version: summary cards, profit per product and per category, missing cost\nwarning, CSV export, foreign currency warning, onboarding, cached results with a\nRecalculate button.<\/li>\n<\/ul>","raw_excerpt":"A free WooCommerce profit report on the native Cost of Goods Sold field: gross margin and gross profit per product, category and period.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/365278","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=365278"}],"author":[{"embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/baltano"}],"wp:attachment":[{"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=365278"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=365278"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=365278"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=365278"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=365278"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/frp.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=365278"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}