Cost of Goods Sold (COGS) is ready to blossom out of beta this summer

Late last year we introduced our roadmap insight for Cost of Goods Sold, aka โ€œCOGSโ€, a feature enhancement to core that assesses the true cost of each product sold.

Here is a reminder of how COGS can benefit merchants for reporting, forecasting, inventory and more: 

  • Calculate exact profit margins for every product and order
  • Influence data-driven decisions about pricing, marketing, and inventory management from COGS data
  • Track a storeโ€™s financial performance with greater precision for sales planning and seasonal initiatives

How can COGS be implemented and utilized by developers?

As a feature, COGS impacts the merchant backend UI, and introduces programmatic API and REST API changes in its implementation. While the feature can currently be found in the experimental features section, it will be moved out of beta in the WooCommerce 10.0 release. Once enabled you can freely use the feature. You don’t have to worry about any changes to your data upon any backward-incompatible improvements in the 10.0 release and beyond.

Let’s check out how you can get COGS enabled and ready for use and exploration with the guide below!

The following guide written by @konamiman will show you how to:

  1. Enable COGS
  2. Set costs for products
  3. Display costs for orders
  4. Clarify terminology in classes
  5. Use classes to handle programmatic changes
  6. Understand REST API updates

Enable COGS

Support for COGS is disabled by default in WooCommerce. To enable it you can go to WooCommerce โ€“ Settings โ€“ Advanced โ€“ Features and tick the โ€œCost of Goods Soldโ€ checkbox.

Set costs for products

Once the feature is enabled youโ€™ll see that the product editor in the admin area has a new โ€œCost of goodsโ€ field in the โ€œGeneralโ€ tab. This will default to zero for all products, you can set any value here and it will be saved together with the rest of the product data.

For variations, the โ€œCost of goodsโ€ field in โ€œGeneralโ€ represents a default value: all the existing variations will inherit this cost value by default, but itโ€™s possible to specify a different value for each individual variation by changing the value directly in the cost box.

Variable products get a bulk action to remove the custom cost from all the variations, so that all of them revert to the default cost value:

Display costs for orders

The order editor in the admin area will have an extra โ€œCostโ€ column for all the product line items in which youโ€™ll see the cost associated to that item (the individual product cost times the quantity of the line). There will be a summary โ€œCost Totalโ€ value next to the total order amount display:

Note: The column that previously had the title โ€œCostโ€ is now โ€œPriceโ€!

If an order has refunds, the โ€œCostโ€ column will display the combined cost of the refunded items:

Additionally, the “Cost” column has a tooltip that displays the cost per unit of the product:

Clarify terminology for classes

The terms โ€œnominalโ€ vs โ€œeffectiveโ€ vs โ€œtotalโ€ values as the terminology can be confusing. Let’s examine how best to use these terms in regards to programmatic changes.

  • The nominal value is the only one thatโ€™s explicitly set (not calculated). Semantically, itโ€™s just a number (or null), and its meaning potentially depends on the type of product and other per-product or store-wide configuration.
  • The effective value is the nominal value converted to a monetary value. As of now, the effective value is always the nominal value, or zero for null nominal values. In future iterations of the feature we might implement other meanings for nominal values (e.g. a percent of the product price) and then the effective value will be calculated accordingly.
  • The total value is the final, actual cost value that applies to the product, and is the value that should be used for any calculations involving costs (itโ€™s used e.g. to calculate aggregate order costs). As of now, this is the same as the effective value, except for variations (as explained above for the get_cogs_value_is_additive method).

Currently, zero and null are equivalent for nominal values: setting zero with set_cogs_value will make get_cogs_value return null. All products will have a null nominal cost value by default.

Use classes to create programmatic changes

As for the code API, there are some additions to the product and order classes so cost values can be handled programmatically.

To start with, use the following snippet to check if the feature is enabled:

 wc_get_container()->get(Automattic\WooCommerce\Internal\Features\FeaturesController::class)->feature_is_enabled('cost_of_goods_sold');
Click the drop down arrow to take a closer look to Products, Hooks, and Order Classes:

 Products

WC_Product gets the following public methods:

  • set_cogs_value( ?float $value ): void โ€“ sets the nominal cost value for the product.
  • get_cogs_value(): ?float โ€“ gets the value set with set_cogs_value.
  • get_cogs_effective_value(): float โ€“ gets the effective cost value for the product (the nominal value converted to a monetary amount).
  • get_cogs_total_value(): float โ€“ gets the total cost value for the product (the value that should be used for actual cost calculations).
  • get_cogs_value_is_additive(): bool โ€“ this method exists in the WC_Product_Variation class only:
    • If true, then get_cogs_total_value will return the parent productโ€™s effective cost value added to the variationโ€™s effective cost value.
    • If false, then get_cogs_total_value will simply return the variationโ€™s effective cost value.

โ€ฆand the following protected methods:

  • adjust_cogs_value_before_set( ?float $value ): ?float โ€“ runs before the value passed to set_cogs_value is applied, by default it just converts zeros to null.
  • get_cogs_effective_value_core(): float โ€“ derived classes willing to use a custom calculation of the effective cost value should override this method.

Note: thereโ€™s currently no UI to change the โ€œcost value is additiveโ€ setting for variations.

Orders

 WC_Abstract_Order gets the following public methods:

  • has_cogs(): bool โ€“ indicates if the current order class handles costs, itโ€™s hardcoded to false and itโ€™s intended to be overridden in derived classes.
  • calculate_cogs_total_value(): float โ€“ calculates the total cost value for the order and then sets it with set_cogs_total_value.
  • set_cogs_total_value( float $value ): void โ€“ sets the total cost value for the order. Normally this method shouldnโ€™t be executed explicitly, but via calculate_cogs_total_value instead.
  • get_cogs_total_value(): float โ€“ returns the value set by set_cogs_total_value.

โ€ฆand the following protected method:

  • calculate_cogs_total_value_core(): float โ€“ performs the actual calculation of the total cost value for the order by adding up the cost for each line item.

The WC_Order class implements the following method overrides:

  • has_cogs()โ€“ returns true.
  • calculate_cogs_total_value_core() โ€“ calculates the value invoking the parent method, and then subtracts the costs associated to refunds.

The last pieces of the puzzle are order items. WC_Order_Item has the following public methods:

  • has_cogs(): bool โ€“ indicates if the current order item class handles costs, itโ€™s hardcoded to false and itโ€™s intended to be overridden in derived classes.
  • calculate_cogs_value(): bool โ€“ calculates the cost value for the line item and sets it with set_cogs_value. Returns true on success or false on error (in case of error the previous value remains unchanged.)
  • set_cogs_value( float $value ): void โ€“ sets the total cost value for the order. Normally this method shouldnโ€™t be executed explicitly, but via calculate_cogs_value instead.
  • get_cogs_value( $context = 'view' ): float โ€“ returns the value set by set_cogs_value.
  • get_cogs_value_html(): string โ€“ returns the cost value in HTML.
  • get_cogs_value_per_unit_tooltip_text(): string โ€“ returns the text to be included in a tooltip indicating the unit cost for the order item.

โ€ฆand the following protected method:

  • calculate_cogs_value_core(): ?float โ€“ performs the actual calculation of the total cost value for the order item. This base method will simply throw an exception, derived classes are expected to override the method with an actual calculation as long as their has_cogs method returns true.

And as you may have guessed already, WC_Order_Item_Product has a couple of overrides:

  • has_cogs returns true.
  • calculate_cogs_value_core returns the total value of the product times the quantity.

And thatโ€™s it! These building blocks provide the basic functionality to handle costs for products and orders, while being extensible enough to add support for costs in custom product, order and order item classes.

Hooks

By the way, the following hooks related to the feature are available:

woocommerce_save_order_cogs_value: to modify the value that gets saved to the database for a given order, or to suppress the value saving altogether.

woocommerce_get_product_cogs_total_value: to customize the value that gets returned by get_cogs_total_value.

woocommerce_load_product_cogs_value: to modify the value that gets loaded from the database for a given product.

woocommerce_load_product_cogs_is_additive_flag: same as above but for the value override flag.

woocommerce_save_product_cogs_value: to modify the value that gets saved to the database for a given product, or to suppress the value saving altogether.

woocommerce_save_product_cogs_is_additive_flag: same as above but for the value override flag.

woocommerce_calculated_order_cogs_value: to customize the value that gets calculated for an order before itโ€™s set.

woocommerce_calculated_order_item_cogs_value: to modify the value that gets calculated for an order item before itโ€™s set.

woocommerce_load_order_item_cogs_value: to modify the value that gets saved to the database for a given order item, or to suppress the value saving altogether.

woocommerce_save_order_item_cogs_value: to modify the value that gets saved to the database for a given order item, or to suppress the value saving altogether.

woocommerce_load_order_cogs_value: to modify the value that gets saved to the database for a given order, or to suppress the value saving altogether.

REST API changes

The following is added to the REST API schema for products and variations (/wp-json/wc/v3/products/<product id> and /wp-json/wc/v3/products/<product id>/variations endpoints, for both GET and POST requests):

{
    "cost_of_goods_sold": {
        "values": [
            {
                "defined_value": <float>
                "effective_value": <float>
            }
        ],
        "total_value": <float>,
        "defined_value_is_additive": <bool>
    }
}

defined_value_is_additive is present only for variations. 

effective_value and total_value are read-only fields.

In principle, for POST endpoints only one object with values is expected to be found inside values.

However the endpoints code will add the defined_values of all objects if more than one is supplied; the result will be the value set for the product.

The POST endpoints will not modify the current value of a product being modified if no values key (or no cost_of_goods_sold key) is present, but an empty values array will set a value of zero.

Similarly, for variations the โ€œvalue overrides parentโ€ flag will not be modified if no defined_value_overrides_parent is present. Default values for new products are a cost of zero and no override for variations.

As for products, the following is added to the REST API schema for the get order(s) endpoint (/wp-json/wc/v3/orders and /wp-json/wc/v3/orders/<order_id>GET only):

{
    "cost_of_goods_sold": {
        "total_value": <float>
    },
    "line_items": [
        {
            "cost_of_goods_sold": {
                "value": <float>
            }
        }
    ]
}

These new items appear only when the feature is enabled.

Thereโ€™s no explicit support for altering order and order items COGS values in the REST API, but any request that triggers a orderโ€™s calculate_totals will cause a recalculation of the COGS values as well.

Finally, the data returned by the order refunds endpoint (/wp-json/wc/v3/orders/<order id>/refunds) will include cost data (as negative values) for both the refund and the refund items, with the same format as for orders.

Reminder: While the feature can currently be found in the experimental features section, it will be moved out of beta in the WooCommerce 10.0 release.


15 responses to “Cost of Goods Sold (COGS) is ready to blossom out of beta this summer”

  1. Justin Avatar

    If we are using the COGS Plugin will this automatically integrate the same way as Brands did?

    1. Shani Banerjee Avatar
      Shani Banerjee

      Hi Justin, it will not auto integrate – the core product CSV import/export tool has been extended to support a COGS field. You can export the cost values, and craft a WooCommerce CSV import file and import it to bring in the data to the core feature.

  2. thx,
    i noticed in ยงDisplay costs for orders, the image-12.png and image-13.png and image-15.png are missiing ๐Ÿ™‚ 403 error
    otherwise, i’m glad to see this coming in v10

    1. Shani Banerjee Avatar
      Shani Banerjee

      Thanks so much Marc, they should work now.

  3. Richard Avatar
    Richard

    Interesting.

    If we use Cost of Goods Sold plugin by SkyVerge, and activate the Cost of Goods Sold beta, will it have any implications on the financial performance? (e.g. Cost x 2).

    Also, will Cost of Goods Sold have exclusion options for calculating profit?
    E.g. Exclude fees charged to customers, shipping charged to customers, and tax charged to customers?

    Thanks.

    1. Shani Banerjee Avatar
      Shani Banerjee

      Hi Richard, sorry for the delay in response, so there are two parts to your question:

      “If we use Cost of Goods Sold plugin by SkyVerge, and activate the Cost of Goods Sold beta, will it have any implications on the financial performance? (e.g. Cost x 2).”

      If beta is installed alongside the SkyVerge extension, you will see two cost totals, since we do not create preference for our feature or an external extension, you will have to decide which one serves you best and prioritize that one. That said, if you go to analytics, our COGS feature does not yet integrate with reports/analytics, so you will only see the SkyVerge output there.

      “Also, will Cost of Goods Sold have exclusion options for calculating profit?
      E.g. Exclude fees charged to customers, shipping charged to customers, and tax charged to customers?”

      Exclusion will not be available in 9.9, and while this has been discussed on the roadmap, I can’t definitively say when we will be committing to implementation.

      I hope these answers help you decide what is best for your current needs!

  4. An issue I’ve had in the past when trying to use Cost of Goods plugins/solutions out there is that the purchase price of something changes over time.

    For example, let’s say we sell fidget spinners. Today I purchased 10k fidget spinners at $1.5 a piece. Over the next two weeks, I sell 8k of them so I buy another batch of 10k pieces. The new price, however, is $1.75 a piece.

    When the second batch of ordered items arrives, the inventory now contains ~2k items with a cost of $1.5 and 10k items with a cost of $1.75.

    I’m not sure what the best solution is here — seems like a more comprehensive inventory solution would be needed. Just wanted to bring up this scenario.

    1. You could average the cost price or add a custom field for another cost price.

    2. Shani Banerjee Avatar
      Shani Banerjee

      Averaging the cost is what is currently suggested.

  5. Already using this but how about calculating tax on profit ( per product ) rather than tax on product price? Any function built in for that?

    1. Shani Banerjee Avatar
      Shani Banerjee

      Hi Brad, not at the moment, but I can certainly submit that as feedback. It might already be roadmapped as I know we have discussed profit/margin features, but no current plans to implement.

  6. Levente Avatar
    Levente

    Is the order item cogs saved in the order item meta table? Let’s say after 1 year prices increase, but that should not change cogs of old orders.

    1. Shani Banerjee Avatar
      Shani Banerjee

      Hi Levente – that’s correct, so it is stored at time of the order. Price changes won’t affect previous orders.

  7. Sridhar Avatar
    Sridhar

    Does the Analytics > Reports also contains this COGS, like Gross Sales, Net sales, etc,…?

    1. Shani Banerjee Avatar
      Shani Banerjee

      Hi Sridhar – not yet. Report and analytics are on the roadmap, so currently you would have to export data to determine those metrics.

Leave a Reply

Your email address will not be published. Required fields are marked *