=== TillFoundry Store Profit Ledger for WooCommerce ===
Contributors: lijnam
Tags: woocommerce, profit, cost of goods, expenses, reports
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Requires Plugins: woocommerce
Stable tag: 1.0.13
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

See what each WooCommerce order really earns you by recording supplier invoices and operating expenses against your orders, products and periods.

== Description ==

A WooCommerce sales report tells you what you took; it does not tell you what you kept. Store Profit Ledger puts supplier invoices, product costs, operating expenses and payment fees in one place next to your orders. A payment fee is shown for its true source: a fee WooCommerce Stripe Gateway or another Stripe gateway plugin, WooCommerce Payments, PayPal Payments or PayPal Standard already persisted on the order is labelled *Actual*; a fee computed from the amount you configure — or the fixed EUR 0.39 fallback for Mollie iDEAL | Wero — is labelled *Estimated*. The plugin never contacts a gateway fee API and never guesses a rate.

**What it does**

* Maintain a supplier directory with contact, tax, bank and default-currency details.
* Record supplier invoices with line items, shipping, deposits and a captured exchange rate; search eligible WooCommerce orders by order number or customer name when linking products, or import invoices from CSV with a preview step.
* Attach a manual unit cost to products and variations, record a product's cost currency and captured exchange rate, and use the target-margin calculator to suggest a selling price.
* Record operating expenses by date, category, currency and captured exchange rate.
* Read per-product-variation profit over any date range, plus a period profit-and-loss dashboard.
* Inspect Profit Reports with one row per order, listing the products bought in it. Shipping, order-wide adjustments and payment fees are allocated across the order's items; report tables are paged and the CSV export streams the same rows.
* Optionally retrieve account-level ad spend from Meta Marketing API and Google Ads API for the selected reporting period. Same-currency spend is included in operating expenses; other-currency amounts are shown separately.
* Download a PDF copy of any supplier invoice or export invoices and reports as CSV.

**Two rules keep the numbers honest**

1. A fee is never dressed up as something it is not. When a supported gateway has already persisted the fee it charged on the order (WooCommerce Stripe Gateway or another Stripe gateway plugin, WooCommerce Payments, PayPal Payments/PPCP or PayPal Standard), that captured figure is used and labelled *Actual*. Otherwise the fee is your configured estimate — or the fixed EUR 0.39 for Mollie iDEAL | Wero, which persists no per-order fee and is therefore always an estimate — and is labelled *Estimated*. The plugin reads no live gateway fee endpoint and converts a captured fee only with captured rate data. An order or product whose cost could not be found is flagged incomplete rather than counted as pure profit.
2. The plugin sends no sales, customer or order data to advertising platforms. Ad-spend requests are opt-in, read-only and limited to account spend for the selected dates. Invoice and expense exchange rates are snapshotted when entered. A rate your installed multi-currency setup already stores locally may be snapshotted for new foreign-currency orders; the plugin never queries an external rate service for exchange rates.

This is practical profit visibility for small and growing merchants, not a full accounting system. It issues no invoices to customers, extracts nothing from PDFs, files no tax returns and tracks no inventory.

This is a free plugin: every feature described above ships in the download, with no paid tier, no licence key and no upgrade prompt.

== Installation ==

1. Install and activate WooCommerce first — Store Profit Ledger requires it.
2. Install the ZIP through **Plugins → Add New → Upload Plugin**, or upload the plugin folder to `/wp-content/plugins/`.
3. Activate the plugin through the **Plugins** screen in WordPress.
4. Open the **Profit Ledger** menu, add a supplier, then record and link an invoice to an order to see that order's cost in Profit Reports.

== Frequently Asked Questions ==

= Does this plugin require WooCommerce? =

Yes. Every report is computed from WooCommerce orders and products, so WooCommerce must be active. On WordPress 6.5 and newer this is enforced when you activate the plugin. Your suppliers, invoices and expenses are stored on your own site and remain in place if you later deactivate the plugin.

= Are the payment fees the real gateway fees? =

When the order carries a fee the gateway already persisted — WooCommerce Stripe Gateway or another Stripe gateway plugin, WooCommerce Payments, PayPal Payments/PPCP or PayPal Standard — the report shows that captured amount and labels it *Actual*. Otherwise the fee is an estimate computed from the percentage and fixed amount you configure (or the fixed EUR 0.39 for Mollie iDEAL | Wero, which persists no per-order fee) and is labelled *Estimated* in the report and the CSV export. The plugin contacts no gateway fee API and converts a captured fee only with a rate already captured locally, so reconcile any *Estimated* figure against your gateway's settlement report. A zero captured fee is reported as a real zero, not replaced by an estimate.

= Does it work with other payment and multi-currency plugins? =

The reports read standard WooCommerce order data — the order currency, line items and refunds — so a plugin that processes orders through WooCommerce's normal order APIs is represented correctly. The ledger does not certify or runtime-test individual payment or multi-currency plugins. A fee is read from order metadata only for the gateways named above and only through guarded checks; every other gateway uses your configured estimate, and Mollie iDEAL | Wero always uses the fixed EUR 0.39 estimate because that integration persists no per-order fee. Several Stripe gateway plugins can be active at the same time — WooCommerce Stripe Gateway and any other Stripe gateway that stores its fee on the order: each order is read against the fee and currency keys written by the gateway that actually processed it, so one plugin's fee is never combined with another's currency. A multi-currency setup's saved order rate is used to convert a foreign-currency order's revenue and cost, and to convert a captured fee. The plugin never invents a rate: a foreign-currency order without a captured rate is marked incomplete until you supply one, and an iDEAL | Wero fee whose EUR rate cannot be derived from captured data is flagged incomplete rather than guessed. The fixed EUR 0.39 is the fallback estimate configured for this release; check the rate your own Mollie contract charges and confirm the estimate against your settlement report.

= How do I connect Meta Ads and Google Ads? =

Open **Profit Ledger → Settings → Advertising connections**. For Meta, enter the ad account ID and an access token with the read-only `ads_read` permission (a Meta Business system-user token assigned to the ad account is suitable for a server-side setup). Pin a currently supported Graph API version. For Google Ads, enter the Ads customer ID, developer token and service-account JSON key; grant that service account access to the Ads account. Set the manager customer ID only when using a manager account to access a client account. Google Ads API reporting requires its developer token in addition to service-account OAuth. Keep these credentials private; they are encrypted in the WordPress database using the site’s WordPress authentication salt. Changing WordPress salts invalidates the saved credentials, which must then be re-entered.

The dashboard and Profit Reports load account-level spend asynchronously after the page appears, so an advertising API delay does not hold up the report. Operating-expense and net-profit cards show that ad spend is loading until they are updated. Platform account timezone and currency apply. Spend in the WooCommerce store’s base currency is included in operating expenses/net profit; mismatched currency is displayed separately and excluded rather than converted using a guessed rate. API outages or invalid credentials are shown as unavailable, not as zero spend. Ad totals are live API report values, not ledger expense records, and the existing product CSV export does not include them. Connections have not been tested against live accounts in this environment.

= Where do the exchange rates come from? =

From you, at the moment you enter a foreign-currency invoice or expense. For new foreign-currency checkout orders, a rate your installed multi-currency setup already stores locally may be snapshotted; the plugin contacts no rate service. Stored rates are never recomputed later.

= What happens when a product has no cost? =

Profit Reports read the frozen order-item cost snapshot first, then a supplier-invoice line linked to that order, then the manual product or variation cost when the selected cost basis allows it. An order with none of these is flagged incomplete, and the affected order is excluded from the profit totals and counted instead. An explicitly linked cost of 0 is treated as a genuinely free product and included in profit.

When **Freeze item costs on new orders** is on, the plugin freezes the resolved unit cost on each order item when checkout completes and when an order moves to processing or completed. Once written, a later invoice edit does not rewrite that item's cost.

= Does uninstalling delete my data? =

Only if you turn on **Delete data on uninstall** in the settings first. By default, deleting the plugin removes nothing but its own dismissible-notice state. Order history is owned by WooCommerce and is never touched.

== Screenshots ==

1. The profit-and-loss dashboard for a month, showing actual/estimated fee labels and the incomplete-cost count.
2. The supplier invoice editor with line items and the deposit/total block.
3. The product profit report with profit and margin columns and an incomplete row flagged.

== External Services ==

The plugin contacts the hosts below only when an administrator has explicitly enabled the corresponding advertising connector and saved its credentials. Both connectors are off by default; until an administrator turns one on, no request to any of these hosts is made. If a request fails for any reason (network error, invalid or expired credentials, or an API error), the affected figures are reported as unavailable rather than as zero, and the plugin continues to work from your locally stored data.

* **Meta Marketing API** — `graph.facebook.com`: when the Meta connector is enabled, the plugin requests the ad-account currency endpoint and the `/act_{account_id}/insights` endpoint, sending the ad account identifier and the requested date range (with the access token as a bearer header), and receives the account currency and account-level spend.
* **Google Ads API** — `oauth2.googleapis.com`: the plugin posts a signed JWT assertion built from the configured service account and receives a short-lived OAuth access token. The assertion carries the OAuth scope `www.googleapis.com/auth/adwords` (its bare host is `googleapis.com`), which authorises the Google Ads report call; the scope string travels inside the assertion and is not a separate request. `googleads.googleapis.com`: the plugin posts a GAQL `googleAds:search` request carrying the Google Ads customer id and the requested date range (with the bearer token and the developer-token header) and receives account-level spend and account currency.

No WooCommerce orders, customer information, product details or other local store records are sent to either service. These external services are governed by [Meta’s terms and privacy policy](https://www.facebook.com/privacy/policy/) and [Google’s terms and privacy policy](https://policies.google.com/privacy). The API credentials are stored encrypted in the site’s WordPress database; no credentials are sent to TillFoundry. No exchange-rate API is used, and there is no licence or update-server call.

== Changelog ==

= 1.0.13 =
* Freeze each order item's resolved unit cost again on checkout and on the processing/completed transitions, matching the **Freeze item costs on new orders** setting.
* Restore the **Cost basis** control on the Ledger settings screen and honour it in Profit Reports again, so a manual product or variation cost is used when no linked invoice cost exists. The select is registered again, so saving the form no longer silently resets the merchant's choice to **Latest supplier invoice linked to the order**.
* Include the configured **Cost basis** in the report cache key, so changing it refreshes a warm report immediately instead of serving the previous basis until the cache expires. A cost snapshot frozen on an order also bumps the report cache version, so a report cached before the order reached processing is rebuilt.
* Render Profit Reports and its CSV export as one row per order with the documented column order.
* Fix a WordPress.org Plugin Check error in the supplier-invoice CSV importer: the UTF-8 BOM probe now uses a WordPress-compatible stream read instead of `fread()`, with the same BOM skip and rewind behaviour. CSV parsing, headers and row limits are unchanged.
* Shorten the 1.0.12 upgrade notice so the directory update screen shows it without truncation.

= 1.0.12 =
* Load Meta and Google advertising spend after the Dashboard and Profit Reports render. Update expense and net-profit figures when the cached account totals arrive, and identify unavailable platform data without blocking the page.
* Calculate reports in bounded order batches and show 50 orders per page, with one row per order item. Stream report CSV rows instead of collecting the full export in memory, and use a stable date-and-ID sort so orders sharing a timestamp cannot be skipped or duplicated.
* Require a linked supplier invoice for report costs, stop writing checkout cost snapshots, and keep existing snapshot metadata untouched.
* Accept BOM-prefixed supplier-invoice CSV exports when importing them again.

= 1.0.11 =
* Price an order from a supplier-invoice line only when that line is linked to the order — the line carries the order id, or it is an order-less line on an invoice that carries the order id. An invoice line linked to no order, or to a different order, no longer supplies a cost, so an order with no linked invoice stays **Cost missing** instead of borrowing another order's cost.
* Keep a variation's manually entered base-currency cost in base currency. The parent product's cost currency and captured rate no longer convert a variation cost that is already in the store currency.
* Save an invoice and its line items together, and roll back a failed CSV invoice instead of reporting it as updated. A failed supplier or expense save now reports the failure rather than a success message.
* Show the order date under the order number on Profit Reports, use a configured-category select on Expenses, warn before leaving an invoice, supplier or expense editor with unsaved changes, and stack phone-width row actions at a 44px touch target.
* Announce supplier logo changes to screen readers, return keyboard focus to **Choose logo** after **Remove logo**, and read count sentences in the singular when the count is one.
* Rename the default **Cost basis** option to **Latest supplier invoice linked to the order** so the settings screen states the linkage rule. A cached report from an earlier version is retired rather than served.

= 1.0.10 =
* Rebuild Profit Reports around one row per **order**: each order now shows the products bought in it, its revenue, costs, fees, **Total cost**, profit, margin and payment provider, newest first. The report CSV exports the same one-row-per-order table, joining an order's product names with semicolons.
* Remove the **Product variation sold** table; its **Export CSV** button now sits on the Orders heading.
* Simplify the **Average per product variation** table: it is no longer split by payment provider, the **Payment provider** column is removed, and the rows are sorted by product name. The Actual/Estimated explanation moves from the Payment fee heading into the fee badge's title.
* Stop drawing the redundant TOTALS caption on the invoice PDF.

= 1.0.9 =
* Group Profit Reports by product **variation** *and* **payment provider**, so the same variation sold through Card and through PayPal appears as two rows instead of one merged row, and two gateway ids that describe the same provider still combine.
* Add a **Total cost** column (cost plus payment fee) to the report and its CSV export, so each variation's full cost is visible beside its profit.
* Remove the **Average per day** section from Profit Reports and its per-day columns from the CSV export; the export now writes one row per variation and provider with the per-1 columns.
* Embed a supplier logo in the invoice PDF whatever its image format. A PNG, GIF or WebP logo was silently dropped while a JPEG worked; every supported format is now included.

= 1.0.8 =
* Rebuild the Profit Reports screen around product variations. The Per order table is removed, and the report now shows one row per product **variation** with what a single unit earned — revenue, cost, payment fee, profit and margin per 1 — alongside the quantity sold.
* Add an **Average per day** section to Profit Reports: the same variations, averaged across the days in the selected date range.
* Remove the per-order CSV export with the per-order table; the CSV export now exports the per-variation report, including the per-1 and per-day columns. An unknown export type is refused.
* Replace the separate PDF Bill To heading, business-name, address, registration-number and phone settings with one **PDF Bill To content** box. Everything you type is printed under the fixed BILL TO heading, one line per line, and the panel grows so a long block is shown in full instead of being cut short.
* Remove the registration-number and phone number the plugin used to add to the PDF Bill To panel on its own; they are now printed only when you type them into the Bill To content box.

= 1.0.7 =
* Read the captured Stripe fee from more Stripe gateways — including the one that stores its fee under `_fkwcs_stripe_fee`, with its own legacy `_stripe_fee` fallback — as well as WooCommerce Stripe Gateway, each fee read together with the currency key its own plugin wrote.
* Verify CSV import size against actual uploaded bytes and avoid repeated refund collection reads across multi-item orders.
* Translate report cost-source labels and improve narrow-screen report tables and invoice-form guidance.
* Preserve actual-versus-estimated payment-fee disclosures.

= 1.0.6 =
* Restore the captured gateway fee paths for WooCommerce Stripe Gateway, WooCommerce Payments, PayPal Payments/PPCP and PayPal Standard, each guarded by gateway ID, fee currency and completeness checks and labelled *Actual*; configured estimates and the fixed Mollie iDEAL | Wero fallback stay labelled *Estimated*.
* Add the fixed EUR 0.39 Mollie iDEAL | Wero payment-fee estimate, converted only with an order's captured rate and skipped for other Mollie methods, other gateways and refunded transactions.
* Export the true fee source in the order and product report CSV. A captured fee keeps the *Actual* label and every estimate keeps the *Estimated* label; no settings control can hide the disclosure.
* Present the advertising connectors as accessible provider panels with credential, scope, caching, failure and data-flow guidance; confirm a save only after the settings and every requested credential write or delete is verified; queue the confirmation and error notices once per save instead of trusting a query flag; and scope settings saves to the plugin capability.
* Customize the supplier-invoice PDF Bill to heading, business name, multiline address, registration-number label and value, and phone from Settings; a blank name, address or phone falls back to the WooCommerce store profile.
* Print the linked WooCommerce order ID on each applicable supplier-invoice PDF line, using the line's own order and only falling back to the invoice order for a genuinely order-linked line.
* Keep description-only custom line items such as "Photography" or "Materials" with no order or catalogue product, accept an unknown SKU-only line using the SKU as its display description, and let the custom-line SKU and Description inputs fill the card on narrow screens.
* Define the supplier-invoice exchange-rate field as store-currency units for one unit of invoice currency. The numeric example is shown only for the currency pair it describes, so it can never point the wrong way.
* Store ledger timestamps through explicit column formats, keep an invoice's order link when a CSV re-import omits it, and match a variation cost on both product and variation.
* Reject non-finite CSV numbers such as overflow literals instead of writing an infinite cost.
* Prorate supplier refunds to product revenue using WooCommerce's per-line refund amounts, keep a refunded item's cost, and report an explicit zero invoice cost as zero rather than missing.
* Replace the invoice form's numbered strip with section anchors, add a narrow-screen cue that the list tables scroll sideways, and warn before ledger edits can be lost when the advertising form is saved.

= 1.0.5 =
* Improve invoice PDF layout and include the configured company registration number in Bill to.
* Show an estimated WooPayments fee for Apple Pay and Google Pay orders, clearly labelled Estimated.
* Show supplier invoice exchange rates as one invoice-currency unit to store currency (for example, USD 1 = NPR 150).

= 1.0.4 =
* Continue long invoice PDFs onto additional pages instead of clipping content.
* Show payment-provider names beside fees in per-order reports and CSV exports.

= 1.0.3 =
* Expand PDF invoice item and totals rows to keep long descriptions and complete amounts visible.
* Show exchange rates as one invoice-currency unit converted to store currency, matching the captured rate.

= 1.0.2 =
* Search supplier-invoice orders by order number or customer name.
* Keep payment fees as clearly labelled merchant-configured estimates.
* Keep estimated fees clearly labelled in reports and CSV exports.

= 1.0.1 =
* Show pending/busy feedback while the supplier-invoice order search and order-product lookups are running, restored on success and failure.
* Restore the manual product and variation cost fields and the target-margin price calculator, and remove third-party product names from the readme.

= 1.0.0 =
* Initial release.

== Upgrade Notice ==

= 1.0.13 =
* Order-item costs are frozen again on checkout and processing, and Profit Reports uses the documented one-row-per-order layout. Supplier-invoice CSV imports detect a UTF-8 BOM with a WordPress-compatible stream read; parsing, headers, row limits and stored ledger data are unchanged.

= 1.0.12 =
* Reports now page order items and load ad spend after the page appears; until totals arrive, expense and net-profit cards say ad spend is loading. Existing cost snapshots are kept but no longer created or used.

= 1.0.11 =
* Costs are read from supplier-invoice lines **linked to the order** only. An unlinked invoice line (or one linked to a different order) no longer prices an order, so some orders that previously showed a borrowed cost now show **Cost missing** until the invoice is linked.

= 1.0.10 =
* Profit Reports is rebuilt around the order: one row per order with its revenue, costs, fees, **Total cost**, profit, margin and payment provider. The report CSV exports that table.

= 1.0.9 =
* Profit Reports now groups by product variation **and** payment provider, adds a Total cost column, and drops the Average per day section. Invoice PDFs now embed a supplier logo in any supported image format, not only JPEG.

= 1.0.8 =
* Profit Reports is now per product variation: no Per order table, a per-1-quantity row for every variation, and an Average per day section.
* The PDF Bill To settings are now one "PDF Bill To content" box, printed line for line under the BILL TO heading.

= 1.0.7 =
* Captured Stripe fees are now read from more Stripe gateways as well as WooCommerce Stripe Gateway, so those orders show an Actual fee. CSV imports enforce size limits against actual uploaded bytes, and refund reporting avoids repeated per-line refund reads.

= 1.0.6 =
* Captured gateway fees are now used and labelled Actual for Stripe, WooPayments, PayPal Payments/PPCP and PayPal Standard; configured estimates and the fixed EUR 0.39 Mollie iDEAL | Wero fallback stay labelled Estimated. PDFs can carry your own Bill To details and the linked order ID.

= 1.0.5 =
* Invoice PDFs have a clearer layout and can include the configured company registration number. Payment fees stay clearly labelled estimates, and supplier-invoice exchange rates show the invoice-to-store direction.

= 1.0.4 =
* Long supplier-invoice PDFs continue onto additional pages with the table header repeated. Per-order reports and report CSV exports now name the payment provider beside each fee, falling back to the gateway identifier.

= 1.0.3 =
* PDF invoice rows and totals now expand to fit content; the exchange-rate label now shows invoice currency to store currency.

= 1.0.2 =
* Search orders by number or customer name when adding order products to a supplier invoice; payment fees remain clearly labelled merchant-configured estimates.

= 1.0.1 =
* Adds loading feedback to the supplier-invoice order search and order-product lookups, restores manual product costs, and cleans up the readme wording.

= 1.0.0 =
* Initial release.
