Skip to content

Limits and guarantees

Profitability Copilot is sold on its figures being worth believing: every one says what it rests on, and none is quietly made up. This page states that promise exactly, including where it stops. Read it before you judge your margins by it.

At a glance

If you are asking The short answer
Will it show profit on orders from before I installed it? Not by itself. Figures start when you switch it on. Back-fill the last 12 months and Recalculate from a date reach back, and label everything they write. More
If I change a product's cost, do old orders change? No. Each line keeps the cost recorded when its order became real. Recalculate, or Recalculate this order, is the only thing that reprices them. More
Can a closed month still change? Yes. Whether an order counts is read from its current status, and returns are read live, so a refund today moves last month. More
Why is my revenue lower than OpenCart's own reports? Uncosted lines are left out of revenue as well as cost, and Revenue is the order lines only: what customers paid for shipping is on a row of its own beneath it. More
Is the shipping my customers pay counted? Yes, once per order, on its own Shipping and order charges row, beside the fulfilment cost it pays for. It is in contribution margin but not in any product's profit. More
Does it know which order line a return came from? No, because OpenCart does not record it. Where one product sits on two lines of one order, the return is allocated to the earliest line. More
Are payment fees read from my payment provider? No. They come from rules you type in per method. An unconfigured method costs zero and is named on the dashboard. More
Does it spread advertising or rent across products? No. Expenses reach the store-wide P&L only, and ROAS is blended across all advertising. More
Does the cost permission hide what I pay my suppliers? It hides the cost field and the cost book. The dashboard and Product profit show cost of goods to anyone who may open them. More
Is the API safe to switch on? Only for a system you trust with every purchase price. An OpenCart API credential cannot be scoped, which is why it ships off. More

Figures start the day you switch it on

Nothing is recorded for an order until the extension is switched on. An order is recorded when it reaches any status other than missing, and the figures are frozen then. An order that completed while the extension was off, in a store where it was off, has no figures, and switching it back on does not fill the gap. Orders placed before you installed it have none either.

Three buttons reach backwards, and nothing else does. Two are on the Maintenance tab of the settings screen:

  • Back-fill the last 12 months records every order of the past twelve months. It goes no further back.
  • Recalculate from a date records again every order placed from the date you give up to today, including orders it already holds.

The third is on one order's line breakdown in Product profit:

  • Recalculate this order records that one order again and touches no other. It asks you to confirm first, and the page reloads with the new figures. It runs even while a dated run is in progress, because recording an order twice at the same costs gives the same figures, and it does not move that run's place. An order that was never placed, or no longer exists, is refused and nothing is written. Each one it records writes one line to the log, naming the order and how many lines it rewrote.

All three use today's costs, and today's fee and fulfilment rules, for orders that were sold under other ones. So everything they write is labelled backfilled, on every screen, every export and every API row, and no later run ever returns a line to actual. A line with no cost recorded stays unknown rather than becoming backfilled, since there is no figure to reconstruct. See how much each figure is worth believing.

A dated run or a back-fill happens in your browser, 25 orders per request, with nothing scheduled behind it. Close the tab and the run stops. Open the Maintenance tab again and Resume the unfinished run continues where it left off. A second run is refused while one is in progress. If a run is abandoned, that refusal lifts fifteen minutes after its last request. Each finished run writes one line to the log, saying what it covered and what the figures it left behind rest on.

The cost is frozen, and some other things are not

The money on each line is frozen when its order becomes real: revenue, unit cost, the share of any discount, the payment fee, the fulfilment cost. Changing a product's cost, or your default margin, changes no figure already recorded. A supplier raising prices next month does not move last month. Setting a cost for a product today does not cost the lines it sold last week either; they stay unknown until you recalculate over them.

Two things are read live, every time a screen loads:

  • Whether an order counts. It counts while its current status is one you nominated (Complete, Shipped and Processing, unless you change the list). An order refunded or cancelled today stops counting today, including in a month you have already closed. The dashboard prints this. An order deleted from OpenCart stops counting too.
  • Returns, from OpenCart's own return records. See returns are an allocation.

The payment fee and fulfilment cost are worked out again whenever the order is recorded again, from the rules as they stand at that moment. That happens on every later status change, on an edit in the admin, on Recheck orders and on a recalculation. The product cost is carried forward in all but the last. So a six-month-old order that moves from Processing to Complete after you changed a gateway's rate is charged the new rate. This is the one deliberate exception to frozen.

The period is keyed on the date the order was placed, the same date OpenCart's own reports use. It is not the date the order reached a counted status. The period you pick on the dashboard is not remembered between visits.

A product nobody has costed is left out, and counted

A line with no cost recorded and no default margin is unknown, and it is excluded from revenue and cost alike. It is not counted at zero cost. Counting it at zero would show a hundred per cent margin on your least-known product. Its sales value is reported beside the coverage figure instead, and the product is listed on Product profit, greyed, with dashes, outside the totals.

The consequence: the revenue on these screens can be well below what OpenCart's own reports say, especially in the first weeks. A default margin turns those lines into labelled estimates. It accepts 0 up to, but not including, 100; 100 is refused because it would derive a cost of zero.

A cost belongs to one product, and nothing is inherited:

  • A variant has its own cost or none. It does not take its master product's cost, because an inherited figure would wear the actual label while being a guess.
  • There is no cost per option value. An option that raises the price raises the line's revenue, and the cost stays the product's.
  • There is no cost history. A product has one cost at a time, with no valid from date.
  • There is no currency on the cost. It is in your store's default currency, which is the currency OpenCart stores order figures in, and every figure here is in that currency.

A cost of 0 means free. An empty field means not costed. Clearing the field removes the cost; it does not write a zero.

Shipping and order charges are counted once per order

The shipping, handling and low-order fees a customer paid are counted on a row of their own, Shipping and order charges, once per order, and added into contribution margin, beside the fulfilment cost you configure, which is deducted. On an order that charged 5.00 for shipping, with a rule saying fulfilment costs 5.00, the two cancel out. The API's profit rows carry the same figure as charges.

The Revenue figure is still the order lines, ex-tax, and nothing else, so it reads lower than OpenCart's own sales total by these charges. They belong to the order, not to any product, so no product's profit includes them, and neither does the Product profit report or the API's product_profit rows. Adding up product profit and adding the charges and Unclassified rows gives contribution margin exactly.

Every figure is ex-tax, apart from the payment fee, which is calculated on the gross amount the customer paid because that is what a gateway charges on. Neither convention is settable; see what you cannot change.

Returns are an allocation

Returns are read from OpenCart's own return records when a screen loads, so a return filed in the admin, by a customer in the storefront or by another extension counts the same way. Editing a return's status corrects the figures on the next page load. Only returns at a status you nominated count: Complete alone, unless you change it.

OpenCart records which product came back, not which line of the order it came from. Where an order holds one product on two lines, typically at two prices because of different options, the return is allocated to the earliest line first. The figure is an allocation, not a fact, and the dashboard says so.

What a return does, by its action:

  • Refunded, Credit Issued, or an action this extension does not know: the returned units' revenue comes off. Their cost comes off too if the return reason is marked resellable. If it is not, you keep the cost, which is what a write-off is. Store credit is treated as a refund whether or not it is ever spent.
  • Replacement Sent: the revenue stays, and one more unit of cost is added.

The returned revenue is the unit's share of the line before any coupon or reward-points discount, and the discount stays counted. On a discounted order, returning everything therefore takes the discount off profit a second time.

More that a buyer tends to assume otherwise:

  • A return lands in the period of the order it was filed against, not the month the goods came back.
  • A return against an order that no longer counts is ignored with the order.
  • A return for more units than the order held is allocated as far as it goes, and the rest is counted on the dashboard as a return that could not be matched. It never produces a negative line.
  • A return that names no order at all, which OpenCart 4.1 allows, belongs to no period and appears nowhere.
  • Dead On Arrival and Faulty ship marked not resellable; every other reason, including one you add yourself, is resellable until you say otherwise.
  • There is no per-return shipping or restocking fee. Record those as an operating expense.

Fees and fulfilment come from rules you write

The payment fee is a percentage of the order's gross total plus a fixed amount, per payment method. The fulfilment cost is a flat amount per order plus a percentage of the shipping charged, or simply equal to the shipping charged, per shipping method. One fulfilment figure covers carriage and packing together. Both are spread across the order's lines in proportion to what each line is worth.

  • Nothing is read from your payment provider. There is no settlement reconciliation, so a fee is what your rule says, not what the provider deducted.
  • A method with no rule costs zero, and its orders still count. The dashboard names every method used in the period that has no rule.
  • There is no weight-based rule. OpenCart does not record the weight an order shipped at, so such a rule would price today's product weight against a year-old order.
  • The settings screen offers a row for each method your orders have actually used, since OpenCart keeps no list of the codes a shipping extension can produce. A method appears there after its first order.

Totals added by other extensions

OpenCart's own coupon and reward-points totals are discounts, spread across the lines. Store credit and gift vouchers are ways of paying, so they change nothing. A total added by another extension is handled by its sign:

  • Positive: counted on the P&L's Unclassified row, by name. It adds to profit and changes no product's figure.
  • Negative: left out of every figure, because it might be a discount that should have been spread across products, and guessing would move a per-product number. The order is named on the dashboard as one whose figures do not add up.

An order whose figures do not add back up to what the customer paid is still counted. Recheck orders on the dashboard works up to a hundred of them out again per press, from the order as it stands.

Operating expenses are store-wide

Rent, software, salaries and advertising are rows you type in on Expenses, once, weekly or monthly, and spread across the days they cover. A row with no end date counts up to today and not beyond. There is no quarterly or yearly frequency, and no import file for expenses.

  • Expenses never reach a product. They come off below contribution margin, on the store-wide P&L, and Product profit never includes them.
  • ROAS is blended: all revenue over all spend on rows marked as advertising. Nothing says which sale came from which advert, and there is no link to Google Ads, Meta or any ad platform. Spend is typed in by hand.
  • Net profit reads as a dash for a period no expense covers, rather than repeating contribution margin under a name that would be false.
  • An expense recorded for every store counts in full in each store's figures, so the stores' net profits do not add up to the group's.

Who can see what you pay

Seeing and setting a cost is a permission of its own, extension/profitability_copilot/catalog/cost, ticked under System → Users → User Groups. Without access a user gets no cost field on the product form and cannot open the cost book. Without modify they cannot change a cost, whatever they post, and the field shows read-only.

The product form cannot refuse a cost it cannot read. The product's own save belongs to OpenCart, so a cost typed as 12,50, €12 or a negative number is ignored when the product saves, with OpenCart's ordinary Success, and the cost stays as it was. The help text under the field says how to write one. The CSV import, by contrast, reports such a line and skips it.

That permission covers the cost itself, not the figures made from it. The dashboard and Product profit have permissions of their own, and each shows cost of goods, per product, to anyone who may open it. Keep those two away from a user group that should not work out your purchase prices.

Expenses and recalculating also have permissions of their own, so a group can read the reports without seeing what you pay people, or without being able to rewrite past figures. The installing group is given all of them.

If a later OpenCart release changes the line of the product form the cost field is placed beside, the field is not shown. The form still works, and every cost already recorded is kept and still editable in the cost book.

The API shows your costs to every API credential

The read-only JSON API ships switched off. Switching it on shows every purchase price and margin to every holder of an OpenCart API credential. Such a credential cannot be limited to one extension, one store or read-only use, and the cost permission above does not reach the API.

The API switch works independently of the extension's own switch. It serves days, not periods. A day with no sales is absent, it has no margin field, and it has no change feed. What each of those means for a client is on the API page.

The cost book file

The cost book takes and gives one CSV of product_id, model, cost: comma-separated, UTF-8, with or without a header.

  • A product the file does not mention keeps its cost. A partial file is safe.
  • An empty cost cell removes that product's cost, so it reads not costed. An untouched export re-imports to exactly what was there.
  • A line is matched on product_id when there is one, and on model otherwise. A model that matches no product, or several, is reported by line number and skipped, never guessed.
  • A line with a cost that is not a figure is reported and skipped, and the stored cost is left alone.

If you also own Import/Export, it can write the cost table directly, journalled and reversible; see guides.

What it does not touch

  • Nothing in your storefront. No template, theme or customer-facing page is changed. On the storefront side it listens for order-status changes and answers the API, and neither renders anything a customer sees.
  • Nothing on OpenCart's order page or product list. There is no profit panel on an order and no cost or margin column in Catalog → Products. Per-product profit is on Product profit, which drills down to orders and lines, and lists every order on its own, the one that lost most first.
  • The margin and markup on the product form read the Price field only. The line under the cost field ignores specials, quantity discounts, customer-group prices and variant overrides, so a product that mostly sells on a special can read healthier there than it is. A price at or below the cost turns that line red and is still saved as you typed it; see what you cannot change.
  • No scheduled task and no email. The insight feed is on the dashboard only. It shows at most five findings from five fixed rules, and says nothing until the period has 30 orders and 60% of its lines are costed. None of that is settable; see what you cannot change.
  • No accounting or ERP sync. Figures leave as CSV or over the API.
  • No customer data. It stores nothing about a person; see what this holds about a person.

Uninstalling and updating

Uninstalling removes the extension's events and settings and nothing else. The four tables, every cost you typed, every expense and every recorded line stay, so that an update, which OpenCart performs as an uninstall and an install, loses nothing. What an update does to your settings is in the changelog.

Nothing in the extension empties those tables. If you want them gone after removing it, ask before touching the database.

It refuses to install below OpenCart 4.0.2.0 or PHP 8.1 rather than installing and recording nothing. See requirements.

Where this page stops

Behaviour that is not built yet is not described here as though it were. If you are weighing Profitability Copilot against a requirement you do not find on this page, on the settings reference or on the API page, assume it is not there, and ask rather than inferring it.