{"id":355912,"date":"2026-08-26T17:34:48","date_gmt":"2026-08-26T17:34:48","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/profit-margin-reports-for-woocommerce\/"},"modified":"2026-08-26T17:34:33","modified_gmt":"2026-08-26T17:34:33","slug":"profit-margin-reports-for-woocommerce","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/profit-margin-reports-for-woocommerce\/","author":17599152,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.1.1","stable_tag":"0.1.1","tested":"7.1","requires":"6.7","requires_php":"7.4","requires_plugins":null,"header_name":"Profit Margin Reports for WooCommerce","header_author":"Profit Margin Reports","header_description":"Turns WooCommerce's built-in Cost of Goods Sold data into the profit and margin reports core does not provide.","assets_banners_color":"162037","last_updated":"2026-08-26 17:34:33","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":58,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.1.1":{"tag":"0.1.1","author":"emtiazzahid","date":"2026-08-26 17:34:33"}},"upgrade_notice":{"0.1.1":"<p>Enqueues the orders-list column stylesheet properly and confirms compatibility with WordPress 7.1.<\/p>","0.1.0":"<p>First public release. Requires WooCommerce 10.3 or newer with both the Cost of Goods Sold feature and WooCommerce Analytics enabled. Only orders placed after Cost of Goods Sold was switched on carry cost data, so earlier orders report as uncosted unless you use estimated mode.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3667541,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3667541,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3667541,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3667541,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3667541,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250},"banner.svg":{"filename":"banner.svg","revision":3667541,"resolution":false,"location":"assets","locale":false}},"assets_blueprints":{"blueprint.json":{"filename":"blueprint.json","revision":3667541,"resolution":false,"location":"assets","locale":"","contents":"{\"$schema\":\"https:\\\/\\\/playground.wordpress.net\\\/blueprint-schema.json\",\"meta\":{\"title\":\"Profit Margin Reports for WooCommerce\",\"description\":\"Turns WooCommerce's Cost of Goods Sold data into the profit and margin reports core does not provide.\",\"categories\":[\"Analytics\",\"eCommerce\"],\"author\":\"profit-margin-reports\"},\"landingPage\":\"\\\/wp-admin\\\/admin.php?page=profit-margin-reports\",\"preferredVersions\":{\"php\":\"8.2\",\"wp\":\"latest\"},\"features\":{\"networking\":true},\"login\":true,\"steps\":[{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"woocommerce\"},\"options\":{\"activate\":true}},{\"step\":\"activatePlugin\",\"pluginPath\":\"profit-margin-reports-for-woocommerce\\\/profit-margin-reports-for-woocommerce.php\"},{\"step\":\"setSiteOptions\",\"options\":{\"woocommerce_feature_cost_of_goods_sold_enabled\":\"yes\",\"woocommerce_analytics_enabled\":\"yes\",\"woocommerce_onboarding_profile\":{\"skipped\":true},\"woocommerce_task_list_hidden\":\"yes\",\"woocommerce_currency\":\"USD\",\"blogname\":\"Profit Margin Reports demo\"}},{\"step\":\"writeFile\",\"path\":\"\\\/tmp\\\/demo-data.php\",\"data\":\"<?php\\n\\\/**\\n * Demo data for the WordPress Playground preview.\\n *\\n * Kept deliberately small: Playground runs WordPress in the browser under WebAssembly, so a\\n * seed that would be trivial on a real host is the difference between a preview that loads and\\n * one visitors abandon. Thirty days of orders is enough for a trend line and a populated\\n * product table.\\n *\\n * Two constraints shape this file and are easy to trip over:\\n *\\n * 1. Action Scheduler does not run, so WooCommerce never imports orders into its Analytics\\n *    lookup tables on its own. The reports read those tables, so we sync each order inline.\\n * 2. Some products are left without a cost on purpose. A demo where everything is perfectly\\n *    covered would hide the coverage meter, which is one of the things worth showing.\\n *\\n * @package ProfitMarginReports\\n *\\\/\\n\\n\\n\\\/**\\n * Seed the preview store.\\n *\\n * Wrapped in a function so its working variables never reach the global scope: this file is\\n * part of the released plugin, and top-level `$order`\\\/`$product` assignments in a shipped\\n * file are both a lint failure and a real collision risk in a shared namespace.\\n *\\n * @return void\\n *\\\/\\nfunction profit_margin_reports_seed_demo_data() {\\n\\tif ( ! function_exists( 'wc_get_product' ) ) {\\n\\t\\treturn;\\n\\t}\\n\\n\\t\\\/\\\/ The blueprint sets this too; belt and braces, because everything below depends on it.\\n\\tupdate_option( 'woocommerce_feature_cost_of_goods_sold_enabled', 'yes' );\\n\\n\\t\\\/\\\/ name, category, price, unit cost (0 means deliberately missing).\\n\\t$catalog = array(\\n\\t\\tarray( 'Aeropress Go Travel Kit', 'coffee-gear', 79.00, 41.00 ),\\n\\t\\tarray( 'Ceramic Pour Over Dripper', 'coffee-gear', 34.00, 12.50 ),\\n\\t\\tarray( 'Burr Grinder Mini', 'coffee-gear', 149.00, 96.00 ),\\n\\t\\tarray( 'Single Origin Ethiopia 1kg', 'beans', 42.00, 27.00 ),\\n\\t\\tarray( 'House Espresso Blend 1kg', 'beans', 31.00, 14.00 ),\\n\\t\\tarray( 'Decaf Colombia 1kg', 'beans', 36.00, 33.50 ),\\n\\t\\tarray( 'Reusable Steel Tumbler', 'accessories', 24.00, 6.00 ),\\n\\t\\tarray( 'Cleaning Tablets 30pk', 'accessories', 18.00, 17.20 ),\\n\\t\\tarray( 'Barista Apron', 'accessories', 45.00, 0.00 ),\\n\\t\\tarray( 'Digital Scale 0.1g', 'coffee-gear', 59.00, 0.00 ),\\n\\t);\\n\\n\\t$product_ids = array();\\n\\n\\tforeach ( $catalog as $row ) {\\n\\t\\tlist( $name, $category, $price, $cost ) = $row;\\n\\n\\t\\t$term = term_exists( $category, 'product_cat' );\\n\\t\\tif ( ! $term ) {\\n\\t\\t\\t$term = wp_insert_term( ucwords( str_replace( '-', ' ', $category ) ), 'product_cat', array( 'slug' => $category ) );\\n\\t\\t}\\n\\n\\t\\t$product = new WC_Product_Simple();\\n\\t\\t$product->set_name( $name );\\n\\t\\t$product->set_status( 'publish' );\\n\\t\\t$product->set_catalog_visibility( 'visible' );\\n\\t\\t$product->set_regular_price( (string) $price );\\n\\t\\t$product->set_price( (string) $price );\\n\\t\\t$product->set_manage_stock( false );\\n\\t\\t$product->set_stock_status( 'instock' );\\n\\t\\tif ( ! is_wp_error( $term ) && isset( $term['term_id'] ) ) {\\n\\t\\t\\t$product->set_category_ids( array( (int) $term['term_id'] ) );\\n\\t\\t}\\n\\n\\t\\tif ( $cost > 0 && method_exists( $product, 'set_cogs_value' ) ) {\\n\\t\\t\\t$product->set_cogs_value( (float) $cost );\\n\\t\\t}\\n\\n\\t\\t$product_id    = $product->save();\\n\\t\\t$product_ids[] = $product_id;\\n\\t}\\n\\n\\t$statuses    = array( 'completed', 'completed', 'completed', 'processing', 'on-hold' );\\n\\t$stats_store = '\\\\Automattic\\\\WooCommerce\\\\Admin\\\\API\\\\Reports\\\\Orders\\\\Stats\\\\DataStore';\\n\\t$prod_store  = '\\\\Automattic\\\\WooCommerce\\\\Admin\\\\API\\\\Reports\\\\Products\\\\DataStore';\\n\\t$made        = 0;\\n\\n\\t\\\/*\\n\\t * Sixty days, not thirty. The dashboard opens on \\\"last 30 days\\\" and compares it with the\\n\\t * thirty before it, so a thirty-day seed leaves every comparison reading as an em dash and the\\n\\t * preview looks broken rather than empty. Sixty days is the smallest span that makes the\\n\\t * comparison real while still booting quickly under WebAssembly.\\n\\t *\\\/\\n\\tfor ( $day = 59; $day >= 0; $day-- ) {\\n\\t\\t\\\/*\\n\\t\\t * $day counts down to today, so days 0-29 are the period the dashboard opens on and\\n\\t\\t * 30-59 are the comparison behind it. Give the recent half more volume: with a flat\\n\\t\\t * distribution every delta lands on roughly 0% and the comparison looks broken in a\\n\\t\\t * different way than an em dash does.\\n\\t\\t *\\\/\\n\\t\\t$is_recent    = $day < 30;\\n\\t\\t$orders_today = ( 0 === $day % 6 ) ? 3 : 2;\\n\\t\\tif ( $is_recent && 0 === $day % 3 ) {\\n\\t\\t\\t++$orders_today;\\n\\t\\t}\\n\\n\\t\\tfor ( $n = 0; $n < $orders_today; $n++ ) {\\n\\t\\t\\t$order = wc_create_order();\\n\\t\\t\\tif ( is_wp_error( $order ) ) {\\n\\t\\t\\t\\tcontinue;\\n\\t\\t\\t}\\n\\n\\t\\t\\t$line_count = 1 + ( ( $day + $n ) % 3 );\\n\\t\\t\\tfor ( $l = 0; $l < $line_count; $l++ ) {\\n\\t\\t\\t\\t$pid     = $product_ids[ ( $day + $n + $l ) % count( $product_ids ) ];\\n\\t\\t\\t\\t$product = wc_get_product( $pid );\\n\\t\\t\\t\\tif ( $product ) {\\n\\t\\t\\t\\t\\t$order->add_product( $product, 1 + ( ( $day + $l ) % 2 ) );\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\n\\t\\t\\tif ( ! $order->get_items() ) {\\n\\t\\t\\t\\t$order->delete( true );\\n\\t\\t\\t\\tcontinue;\\n\\t\\t\\t}\\n\\n\\t\\t\\t$order->set_billing_first_name( 'Demo' );\\n\\t\\t\\t$order->set_billing_last_name( 'Customer ' . ( ( $day * 10 ) + $n ) );\\n\\t\\t\\t$order->set_billing_country( 'US' );\\n\\t\\t\\t$order->set_date_created( gmdate( 'Y-m-d H:i:s', time() - ( $day * DAY_IN_SECONDS ) - ( $n * HOUR_IN_SECONDS ) ) );\\n\\n\\t\\t\\t\\\/\\\/ calculate_totals() is what records the per-line cost snapshot.\\n\\t\\t\\t$order->calculate_totals( false );\\n\\n\\t\\t\\t$status = $statuses[ ( $day + $n ) % count( $statuses ) ];\\n\\t\\t\\t$order->set_status( $status );\\n\\t\\t\\t$order->set_date_paid( $order->get_date_created() );\\n\\t\\t\\t$order_id = $order->save();\\n\\n\\t\\t\\t\\\/\\\/ Refund one order a week so the refund handling is visible in the demo.\\n\\t\\t\\t$refund_id = 0;\\n\\t\\t\\tif ( 0 === $day % 7 && 0 === $n && 'completed' === $status ) {\\n\\t\\t\\t\\t$items = $order->get_items( 'line_item' );\\n\\t\\t\\t\\t$first = reset( $items );\\n\\t\\t\\t\\tif ( $first && $first->get_quantity() > 0 ) {\\n\\t\\t\\t\\t\\t$unit = (float) $first->get_total() \\\/ max( 1, $first->get_quantity() );\\n\\t\\t\\t\\t\\tif ( $unit > 0 ) {\\n\\t\\t\\t\\t\\t\\t$refund = wc_create_refund(\\n\\t\\t\\t\\t\\t\\t\\tarray(\\n\\t\\t\\t\\t\\t\\t\\t\\t'order_id'   => $order_id,\\n\\t\\t\\t\\t\\t\\t\\t\\t'amount'     => round( $unit, 2 ),\\n\\t\\t\\t\\t\\t\\t\\t\\t'reason'     => 'Demo refund',\\n\\t\\t\\t\\t\\t\\t\\t\\t'line_items' => array(\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t$first->get_id() => array(\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t'qty'          => 1,\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t\\t'refund_total' => round( $unit, 2 ),\\n\\t\\t\\t\\t\\t\\t\\t\\t\\t),\\n\\t\\t\\t\\t\\t\\t\\t\\t),\\n\\t\\t\\t\\t\\t\\t\\t)\\n\\t\\t\\t\\t\\t\\t);\\n\\t\\t\\t\\t\\t\\tif ( $refund && ! is_wp_error( $refund ) ) {\\n\\t\\t\\t\\t\\t\\t\\t$refund_id = $refund->get_id();\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\n\\t\\t\\t\\\/*\\n\\t\\t\\t * Action Scheduler never runs here, so WooCommerce would leave its Analytics lookup\\n\\t\\t\\t * tables empty and every report would read zero. Sync inline instead.\\n\\t\\t\\t *\\\/\\n\\t\\t\\t$to_sync = array( $order_id );\\n\\n\\t\\t\\t\\\/*\\n\\t\\t\\t * A refund is its own order record, and WooCommerce writes a separate lookup row for\\n\\t\\t\\t * it. Syncing only the parent leaves the refund unimported, which the plugin correctly\\n\\t\\t\\t * reports as \\\"some orders are missing from Analytics\\\" - accurate, but a warning banner\\n\\t\\t\\t * is not what the preview should open with.\\n\\t\\t\\t *\\\/\\n\\t\\t\\tif ( $refund_id ) {\\n\\t\\t\\t\\t$to_sync[] = $refund_id;\\n\\t\\t\\t}\\n\\n\\t\\t\\tforeach ( $to_sync as $sync_id ) {\\n\\t\\t\\t\\tif ( is_callable( array( $stats_store, 'sync_order' ) ) ) {\\n\\t\\t\\t\\t\\tcall_user_func( array( $stats_store, 'sync_order' ), $sync_id );\\n\\t\\t\\t\\t}\\n\\t\\t\\t\\tif ( is_callable( array( $prod_store, 'sync_order_products' ) ) ) {\\n\\t\\t\\t\\t\\tcall_user_func( array( $prod_store, 'sync_order_products' ), $sync_id );\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\n\\t\\t\\t++$made;\\n\\t\\t}\\n\\t}\\n\\n\\t\\\/\\\/ Stop WooCommerce showing an \\\"importing analytics\\\" banner over the demo.\\n\\tupdate_option(\\n\\t\\t'woocommerce_admin_import_stats',\\n\\t\\tarray(\\n\\t\\t\\t'imported_from' => '0',\\n\\t\\t\\t'is_importing'  => false,\\n\\t\\t)\\n\\t);\\n\\n\\tif ( defined( 'WP_CLI' ) && WP_CLI ) {\\n\\t\\tWP_CLI::log( \\\"Playground demo data: {$made} orders across \\\" . count( $product_ids ) . ' products.' );\\n\\t}\\n}\\n\\n\\\/*\\n * Deliberately NOT self-executing. The Playground blueprint requires this file and then\\n * calls the function itself, so a trailing call here would seed the store twice - which it\\n * silently did, producing double the products and orders.\\n *\\\/\\n\"},{\"step\":\"runPHP\",\"code\":\"<?php require_once '\\\/wordpress\\\/wp-load.php'; require_once '\\\/tmp\\\/demo-data.php'; profit_margin_reports_seed_demo_data();\"},{\"step\":\"installPlugin\",\"pluginData\":{\"resource\":\"wordpress.org\\\/plugins\",\"slug\":\"profit-margin-reports-for-woocommerce\"},\"options\":{\"activate\":true}}]}"}},"all_blocks":[],"tagged_versions":["0.1.1"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3667541,"resolution":"1","location":"assets","locale":"","width":1280,"height":800},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3667541,"resolution":"2","location":"assets","locale":"","width":1280,"height":800},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3667541,"resolution":"3","location":"assets","locale":"","width":1280,"height":800},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3667541,"resolution":"4","location":"assets","locale":"","width":1280,"height":800}},"screenshots":{"1":"The profit summary for a date range: net revenue, cost of goods, gross profit and gross margin, each compared with the previous period, above the daily gross profit series.","2":"Product and variation profitability, sorted by gross profit, with the cost coverage of each row shown alongside so partly costed products are obvious.","3":"The Profit column on the WooCommerce orders list, calculated from the cost snapshot stored on each order.","4":"The backfill assistant setting product costs in bulk from a percentage of the regular price, with a preview of what will change before anything is written."}},"plugin_section":[],"plugin_tags":[233971,247065,11938,6882,286],"plugin_category":[45],"plugin_contributors":[191403],"plugin_business_model":[],"class_list":["post-355912","plugin","type-plugin","status-publish","hentry","plugin_tags-cogs","plugin_tags-cost-of-goods-sold","plugin_tags-margin","plugin_tags-profit","plugin_tags-woocommerce","plugin_category-ecommerce","plugin_contributors-emtiazzahid","plugin_committers-emtiazzahid"],"banners":{"banner":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/banner-772x250.png?rev=3667541","banner_2x":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/banner-1544x500.png?rev=3667541","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/icon.svg?rev=3667541","icon":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/icon.svg?rev=3667541","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/screenshot-1.png?rev=3667541","caption":"The profit summary for a date range: net revenue, cost of goods, gross profit and gross margin, each compared with the previous period, above the daily gross profit series."},{"src":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/screenshot-2.png?rev=3667541","caption":"Product and variation profitability, sorted by gross profit, with the cost coverage of each row shown alongside so partly costed products are obvious."},{"src":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/screenshot-3.png?rev=3667541","caption":"The Profit column on the WooCommerce orders list, calculated from the cost snapshot stored on each order."},{"src":"https:\/\/ps.w.org\/profit-margin-reports-for-woocommerce\/assets\/screenshot-4.png?rev=3667541","caption":"The backfill assistant setting product costs in bulk from a percentage of the regular price, with a preview of what will change before anything is written."}],"raw_content":"<!--section=description-->\n<p>WooCommerce ships the cost data. It does not ship the report.<\/p>\n\n<p>With the Cost of Goods Sold feature enabled, WooCommerce stores a cost for each product and stamps a cost snapshot onto every order line as the order is placed. Then it stops. WooCommerce's own documentation states that Cost of Goods Sold does not integrate with WooCommerce Analytics, and the request to build profit reporting on top of it was closed as not planned.<\/p>\n\n<p>Profit Margin Reports for WooCommerce is that missing report. It reads the cost data WooCommerce already holds and turns it into gross profit, margin, per-product profitability and a per-order profit figure. Nothing is sent anywhere, nothing is rewritten, and no number is presented as more certain than the data behind it.<\/p>\n\n<h4>What you get<\/h4>\n\n<ul>\n<li><strong>Profit summary<\/strong> for any date range: net revenue, cost of goods, gross profit and gross margin, with the previous period beside it for comparison.<\/li>\n<li><strong>A daily series<\/strong> of revenue, cost and gross profit, so you see the trend instead of one number.<\/li>\n<li><strong>Product and variation profitability<\/strong>, sortable by gross profit, net revenue, cost, units sold or margin, so the products quietly losing money stop being invisible.<\/li>\n<li><strong>A profit column on the orders list<\/strong>, taken from the cost snapshot stored on that order's own line items.<\/li>\n<li><strong>Coverage shown next to every total.<\/strong> Each figure tells you how many of the order lines behind it actually carried cost data. A profit number built from 40 percent of your lines is labelled as such rather than presented as fact.<\/li>\n<li><strong>An optional estimated mode<\/strong> that fills lines with no cost snapshot using the product's current cost. Estimates are labelled everywhere they appear and are never silently blended into actual figures.<\/li>\n<li><strong>A cost backfill assistant<\/strong> for entering product costs in bulk, either as a percentage of the regular price or by importing a CSV of your own costs. Both show you exactly what would change before anything is written. When it writes a variation cost it also clears WooCommerce's per-variation \"additive\" flag, so a stale flag left behind by an importer cannot silently double that variation's cost.<\/li>\n<\/ul>\n\n<h4>How these numbers relate to Analytics<\/h4>\n\n<p>Reports honour the same excluded order statuses as WooCommerce Analytics, and refunds are netted the way Analytics nets them: a refund reduces revenue, units and cost on the date the refund was issued.<\/p>\n\n<p>Every figure is keyed on the order creation date, because the WooCommerce line-item table these reports are built from stores no other date. WooCommerce Analytics defaults its own screens to the paid date. On most stores those are the same day and the totals agree, but on a store that takes payment later than the order is placed, such as bank transfer or invoicing, the two will legitimately disagree about which day a sale lands on.<\/p>\n\n<h4>Read this before you install<\/h4>\n\n<p>A profit report is only ever as good as the cost data underneath it. Here is exactly what this plugin can and cannot know.<\/p>\n\n<ul>\n<li><strong>It reports on cost data, it does not invent it.<\/strong> If a product has no cost entered, there is no profit to calculate for it.<\/li>\n<li><strong>Orders placed before you enabled Cost of Goods Sold have no cost at all.<\/strong> WooCommerce writes the cost onto the order line as the order is created, so entering costs later does not travel backwards. Historical orders show as uncovered. Estimated mode applies today's cost to yesterday's order, which is useful for a rough view but is an estimate, not history.<\/li>\n<li><strong>A cost of exactly zero is indistinguishable from no cost.<\/strong> WooCommerce stores a zero cost by deleting the value, so nothing downstream can tell a real zero from a blank. This plugin never reports a 100 percent margin for a line with no cost: such lines are counted as uncovered and excluded from margin.<\/li>\n<li><strong>WooCommerce Analytics must be enabled.<\/strong> The reports read its lookup tables. The plugin also warns you when WooCommerce has orders it has not yet imported into them, because an incomplete import means an incomplete report.<\/li>\n<li><strong>This release queries your order tables live.<\/strong> There is no separate pre-aggregated profit table yet. Every report is calculated when you load it, so on a store with a very large order history a long date range can be slow.<\/li>\n<li><strong>One currency at a time.<\/strong> If your orders span more than one currency the plugin tells you, rather than adding different currencies together into a meaningless total.<\/li>\n<\/ul>\n\n<h4>Requirements<\/h4>\n\n<ul>\n<li>WooCommerce 10.3 or newer.<\/li>\n<li>The Cost of Goods Sold feature enabled at WooCommerce &gt; Settings &gt; Advanced &gt; Features.<\/li>\n<li>WooCommerce Analytics enabled.<\/li>\n<li>PHP 7.4 or newer, WordPress 6.7 or newer.<\/li>\n<li>Works with High-Performance Order Storage and with legacy post-based order storage.<\/li>\n<\/ul>\n\n<h3>Privacy<\/h3>\n\n<p>This plugin makes no external HTTP requests and sends no data anywhere, to its author or to anyone else.<\/p>\n\n<p>There is no telemetry, no usage tracking, no analytics beacon, no phone-home on activation or deactivation, no remote licence check, and no third-party service of any kind. Every figure it shows is calculated from your own database on your own server, and nothing about your store, your orders, your costs or your customers leaves it.<\/p>\n\n<p>It sets no cookies, registers no front-end assets and adds nothing to the customer-facing side of your store. It is an admin-only reporting screen.<\/p>\n\n<h3>Source and build<\/h3>\n\n<p>The compiled files in build\/ are generated from the human-readable source in src\/, which ships with the plugin. To rebuild them:<\/p>\n\n<pre><code>npm ci &amp;&amp; npm run build\n<\/code><\/pre>\n\n<p>This uses @wordpress\/scripts and requires Node 20 or newer. package.json and package-lock.json ship too, so the build is reproducible from the released files alone.<\/p>\n\n<p>The build also compiles in nine ISC-licensed d3 packages: d3-array, d3-color, d3-format, d3-interpolate, d3-path, d3-scale, d3-shape, d3-time, and internmap. Their full licence text and copyright notices ship with the plugin in build\/index.js.LICENSE.txt, generated at build time from each package's own licence file.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install and activate WooCommerce 10.3 or newer.<\/li>\n<li>Go to WooCommerce &gt; Settings &gt; Advanced &gt; Features and enable <strong>Cost of Goods Sold<\/strong>. Until this is on, WooCommerce records no costs at all and every report will be empty.<\/li>\n<li>Install this plugin from Plugins &gt; Add New, or upload the plugin folder to <code>\/wp-content\/plugins\/<\/code>, then activate it.<\/li>\n<li>Enter costs for your products. The field is <strong>Cost of goods<\/strong> on the Product data &gt; General panel. To fill in many products at once, use the backfill assistant on the plugin's own screen.<\/li>\n<li>Open <strong>WooCommerce &gt; Profit<\/strong> to view your reports.<\/li>\n<\/ol>\n\n<p>Only orders created after step 2 carry a cost snapshot, so expect the report to start filling in from that point forward. Use estimated mode if you need an approximate picture of the period before that.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"why%20are%20my%20reports%20empty%3F\"><h3>Why are my reports empty?<\/h3><\/dt>\n<dd><p>Work down this list, in order. The plugin's own status panel checks all of these and tells you which one is failing.<\/p>\n\n<ol>\n<li>The Cost of Goods Sold feature is off. It is off by default in WooCommerce. While it is off, every cost value WooCommerce returns is zero and nothing is recorded. Enable it at WooCommerce &gt; Settings &gt; Advanced &gt; Features.<\/li>\n<li>WooCommerce Analytics is off. The reports read the Analytics lookup tables, so with Analytics disabled there is nothing to query.<\/li>\n<li>Your products have no cost entered. Set <strong>Cost of goods<\/strong> on each product, or use the backfill assistant.<\/li>\n<li>Your orders predate the feature being enabled. See the next question.<\/li>\n<li>WooCommerce has orders waiting to be imported into its Analytics tables. Run the Analytics import from WooCommerce &gt; Status &gt; Tools and let it finish.<\/li>\n<\/ol><\/dd>\n<dt id=\"why%20do%20my%20old%20orders%20show%20no%20cost%3F\"><h3>Why do my old orders show no cost?<\/h3><\/dt>\n<dd><p>Because there is no cost stored on them. WooCommerce copies a product's cost onto the order line at the moment the order is created, which is what makes historical profit accurate even after you change a supplier price. The flip side is that orders created before the Cost of Goods Sold feature was switched on have no cost snapshot at all, and entering product costs today does not retroactively write costs onto those old orders. No plugin can recover data WooCommerce never wrote.<\/p>\n\n<p>Two things help. Coverage reporting tells you honestly how much of a period is uncosted, and estimated mode applies each product's current cost to those uncosted lines so you get an approximate figure. An estimate is always labelled as an estimate.<\/p><\/dd>\n<dt id=\"how%20does%20the%20csv%20cost%20import%20work%3F\"><h3>How does the CSV cost import work?<\/h3><\/dt>\n<dd><p>Upload a CSV containing a product identifier and a cost, then map which column is which. The plugin shows you every row it matched and every row it could not, and writes nothing until you confirm.<\/p>\n\n<p>The uploaded file is validated by content rather than by its name, stored under a generated filename so nothing you upload can choose where it lands, and deleted in the same request that parses it. Costs are written through WooCommerce's own product API, never by direct database writes. A template CSV of your existing products can be downloaded from the same screen; values in it are escaped so a product name or SKU beginning with an equals sign cannot execute as a formula when the file is opened in a spreadsheet.<\/p><\/dd>\n<dt id=\"does%20this%20plugin%20send%20my%20data%20anywhere%3F\"><h3>Does this plugin send my data anywhere?<\/h3><\/dt>\n<dd><p>No. It makes no external HTTP requests of any kind. No telemetry, no tracking, no remote licence check, no third-party services. All calculation happens in your own database on your own server. See the Privacy section above.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20hpos%3F\"><h3>Does it work with HPOS?<\/h3><\/dt>\n<dd><p>Yes. The plugin declares compatibility with High-Performance Order Storage and reads order data correctly under both HPOS and legacy post-based storage. It also declares compatibility with the Cost of Goods Sold feature itself.<\/p><\/dd>\n<dt id=\"does%20it%20change%20my%20cost%20data%3F\"><h3>Does it change my cost data?<\/h3><\/dt>\n<dd><p>No. Reporting is strictly read-only: it queries WooCommerce's tables and writes nothing back to your products or orders.<\/p>\n\n<p>The single exception is the backfill assistant, which exists specifically to write product costs, and only does so for the products you select, after you confirm, and using WooCommerce's own product API rather than direct database writes. It never touches costs already recorded on existing orders, because those are a historical record.<\/p><\/dd>\n<dt id=\"do%20the%20numbers%20match%20woocommerce%20analytics%3F\"><h3>Do the numbers match WooCommerce Analytics?<\/h3><\/dt>\n<dd><p>Usually, and where they differ there is a reason worth knowing.<\/p>\n\n<p>The plugin honours the same excluded order statuses configured in your Analytics settings and nets refunds the same way. The difference is the date. These reports key every figure on the order creation date, because the WooCommerce line-item table they read stores no other date, while Analytics defaults to the paid date. If your store is paid at checkout those are the same day and the totals agree. If payment can arrive days after the order, the same sale can land on different days in the two reports.<\/p><\/dd>\n<dt id=\"how%20are%20refunds%20handled%3F\"><h3>How are refunds handled?<\/h3><\/dt>\n<dd><p>WooCommerce writes a refund as its own negative line, carrying negative quantity and a negative cost, dated on the day the refund was issued. Reports sum those lines along with everything else, so a refund reduces revenue, units and cost on the refund date. The refunded order is not retrospectively rewritten.<\/p><\/dd>\n<dt id=\"what%20does%20the%20coverage%20percentage%20mean%3F\"><h3>What does the coverage percentage mean?<\/h3><\/dt>\n<dd><p>It is the share of order lines in the current report that had a cost recorded. At 100 percent, every line was costed and the profit figure is complete. At 60 percent, four lines in ten contributed revenue but no cost, so the true cost is higher and the true profit lower than shown. Coverage is displayed rather than hidden precisely so that a partly costed report is never mistaken for a complete one.<\/p><\/dd>\n<dt id=\"which%20orders%20count%2C%20and%20who%20can%20see%20the%20reports%3F\"><h3>Which orders count, and who can see the reports?<\/h3><\/dt>\n<dd><p>The same orders WooCommerce Analytics counts: everything except the statuses excluded in your Analytics settings, plus draft, auto-draft and trashed orders, which are never counted because they are not sales. Developers can adjust this with the <code>profit_margin_reports_report_statuses<\/code> filter.<\/p>\n\n<p>Anyone with the <code>view_woocommerce_reports<\/code> capability, which by default means shop managers and administrators. Writing costs through the backfill assistant additionally requires <code>manage_woocommerce<\/code>.<\/p><\/dd>\n<dt id=\"is%20a%20very%20large%20store%20going%20to%20be%20slow%3F\"><h3>Is a very large store going to be slow?<\/h3><\/dt>\n<dd><p>Possibly, in this release. Reports are calculated live against your order tables on each load, with a short-lived cache. Stores with hundreds of thousands of orders may find long date ranges take a while. Pre-aggregation is planned.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>0.1.1<\/h4>\n\n<p>Review feedback from the WordPress.org plugin team.<\/p>\n\n<ul>\n<li>The orders-list column stylesheet is now enqueued through wp_add_inline_style() on a registered handle rather than printed as an inline style tag.<\/li>\n<li>Tested against WordPress 7.1.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<p>Initial release.<\/p>\n\n<ul>\n<li>Profit summary with net revenue, cost of goods, gross profit, gross margin and previous-period comparison.<\/li>\n<li>Daily revenue, cost and gross profit series.<\/li>\n<li>Product and variation profitability report with sorting and per-row cost coverage.<\/li>\n<li>Profit column on the WooCommerce orders list.<\/li>\n<li>Cost coverage reporting on every total, so uncosted lines are never mistaken for zero-cost lines.<\/li>\n<li>Optional estimated mode using current product costs for lines with no cost snapshot.<\/li>\n<li>Cost backfill assistant: set costs as a percentage of the regular price, or import them from a CSV, both with a preview before anything is written.<\/li>\n<li>Variation costs are written as absolute values, clearing WooCommerce's per-variation additive flag so a stale flag cannot double a cost.<\/li>\n<li>Status panel reporting whether Cost of Goods Sold, Analytics, order storage and Analytics imports are in a state that allows accurate reporting.<\/li>\n<li>Compatible with High-Performance Order Storage and with the Cost of Goods Sold feature.<\/li>\n<\/ul>","raw_excerpt":"Gross profit and margin reports built from the Cost of Goods Sold data WooCommerce already stores but never reports on.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/355912","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=355912"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/emtiazzahid"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=355912"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=355912"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=355912"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=355912"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=355912"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=355912"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}