Food Products for WooCommerce turns your WooCommerce shop into a food shop. It adds two
new product types -
Simple food product and
Variable food product - that work exactly
like WooCommerce's own Simple and Variable products, with everything a food or grocery shop
needs on top: a nutrition declaration table, expiry dates, batch and supplier tracking, and
automatic handling of stock that is about to go off.
Nothing is replaced or overridden: your existing products, themes and payment gateways carry
on untouched. You choose the new product type only on the products that need it.
Nutrition declaration table
Every food product gets a
Nutrition tab: a label/amount/unit table declared per 100 g or
per 100 ml (pre-filled with the EU-mandatory energy / fat / carbohydrate / protein / salt rows,
all editable) that is shown to shoppers as its own tab on the product page. Rows can be
reordered, renamed, indented as sub-nutrients, added and removed. Variable food products can
share one table across every variation, or let a single variation override it with its own.
Ingredient list
An
Ingredients tab holding the ingredients in declaration order, each with an optional
percentage, sub-ingredients and one of the 14 EU-declarable allergens. The product page prints
the list as one sentence with allergens in bold, plus a "may contain" line. Per-variation
overrides work the same way as for nutrition.
Expiry dates and batch tracking
The
Expiry & Tracking tab stores one row per batch, each with an expiry date, quantity,
batch number, batch date and supplier - so you always know which delivery a unit of stock came
from and when it goes off. Variable food products track batches per variation, matching how
WooCommerce already tracks stock. A full-price sale takes units off the freshest batch first, a
reduced sale takes them off the batch the shopper picked, and a restocking refund puts them back,
so the table reads as what is still on the shelf.
Optionally, the closest upcoming expiry date is shown to shoppers on the product page - always,
or only once it falls within a number of days you choose.
Expiry Dashboard and write-off
An admin screen listing every batch row across every food product in one place, sorted by
closest expiry date first, colour-coded by urgency, filterable by date range and exportable as
a CSV file for your stock takes and audits. A batch that has expired can be written off from
there or from the product itself, taking what is left of it out of the sellable stock - or you
can have that happen automatically every night at an hour you choose.
The batch table is the stock
Once a product tracks batches, its batch table
is its stock. WooCommerce's own stock
management is switched off for it while it tracks; the product is in stock while a batch in date
has units left, "N in stock" on the product page is the batch total, and a shopper cannot buy
more than the batches hold. Every sale comes off a batch - a full-price sale off the freshest
batch first, so the batch nearest its date is kept for the reduced offer - and a restocking refund
puts the units back. Stock past its best-before date is not sold at all unless you say so (see
below). Switch tracking off on a product, change it to a non-food type, or turn Expiry & Tracking
off in the settings, and WooCommerce's stock management comes back as it was before - with the
batch total as its stock quantity, since every sale in between came off the batches.
Automatic Markdown
Offer a discount on batches close to their date, on a ladder of steps you set (for example
15% a week out, 30% three days out, 50% on the last day). The product's own price is never
changed: the shopper opts into a reduced batch on the product page, choosing the date they want,
the discount applies to that cart line only, and an offer withdraws itself once its batch is sold
out or expired. A cap on units per batch, a price floor, a catalogue badge and a choice of whether
a product with nothing left to sell stays listed or is hidden are all yours to set, and any product
can opt out.
Where the law allows it, you can also choose to keep selling batches past their best-before
date: that adds a fixed last step to the ladder with its own discount and wording, and those
units count as sellable stock. A
Markdown Earnings
screen reports what each marked-down batch sold, what it brought in, what the discount cost, and
how much expired unsold anyway. Sell-through is worked out from your current settings, so changing an
opt-out, the category scope, the ladder or expired sales later also changes the figures of past batches.
Email when expired
A scheduled digest email listing every batch that has reached its expiry date - or is about to,
within a window you choose - sent to the shop users you pick, daily, weekly, every 2 weeks or
every 3 weeks. Each batch is mailed once to each recipient, every send is recorded in an Email
Log, and a "Send one now" button lets you run it on demand. Individual products can opt out.
Automatic category switching
Have WooCommerce move stock for you: categories you choose (for example "Clearance" or "Last
chance") are added to a product as it nears its closest expiry date, and categories are removed
- or every category stripped - once that date has passed. If a fresh, later-dated batch pushes
the expiry date back out, both changes are reversed automatically, and categories you assigned
by hand are never touched. An optional Switching Log screen records every move the nightly
run makes, with the product, the direction and the batch that caused it.
Built for real shops
- Fully translation-ready, with a choice of date formats for the dates it prints.
- WooCommerce HPOS (High-Performance Order Storage) and Cart & Checkout Blocks
compatible (see Requirements below).
- No account, no subscription, no external service: the plugin never contacts a remote server
and sends no analytics or telemetry of any kind. Everything runs on your own site.
- No bundled third-party libraries - it uses the copies of jQuery and Select2 that WordPress
and WooCommerce already ship.
- Every file shipped is unminified, human-readable source. There is no build step.
Requirements
WooCommerce 10.0 or later must be installed and active. The plugin will not activate without
it. If WooCommerce is later disabled: on a single site, the plugin deactivates itself with an
explanatory notice the next time an administrator opens wp-admin; on a network activation the
plugin stays active and shows a notice on sites without WooCommerce, where its features stay
off. MySQL 5.7 or later, or MariaDB 10.0.2 or later, is also required - the stock locks nest
named locks.
2.1.1
- Fixed: switching Expiry & Tracking off hands stock management back for every product, including trashed ones, those past the first 50 and ones changed through the REST API, Bulk Edit or Quick Edit; switching it back on takes the units sold meanwhile off the batches.
- Fixed: stock stays right in more cases. Saving a batch table from a stale page no longer revives deleted batches or drops new ones, a batch change the database did not store is reported, removing an order line puts its units back, held units of deleted orders are kept until credited back, and tracked products sell in whole units only.
- Fixed: unpaid checkouts hold their units more reliably, and a markdown line that was withdrawn stops a classic or Store API checkout instead of charging the reduced price.
- Fixed: Variable food products match Variable products in the REST API, structured data (AggregateOffer), the shared stock pool and the Ingredients and Nutrition tabs.
- Fixed: the categories chosen for category switching and the Automatic Markdown scope stay attached when a category's slug is edited, and category switching re-checks each product before acting.
- Fixed: the expiry digest mails a recipient added later the batches the others already had, and nobody gets the same batch twice. A batch exactly on the "Days before expiration" day now counts as nearing expiry.
- Fixed: Markdown Earnings subtracts refunds made against a whole order, finds sold batches of deleted products by SKU, and its CSV no longer shows "&". The Switching Log CSV gains a Reason column and shows category names as on screen.
- Fixed: deleting the last batch on an Expiry Dashboard page loads a page with batches and shows a "Batch deleted." notice.
- Fixed: settings with an out-of-range, half filled or duplicate discount step, or a list posted for a single-value field, are refused with an error naming the field.
- Improved: right-to-left layout and accessibility on the admin screens, number and date formats follow the site's language, translations use whole sentences, and the Appearance screen reads the Site Editor's custom colours.
- Improved: security hardening of request handling and output, plus lighter page loads: pages other than product pages skip the offer lookup, and the nightly job skips the catalogue while its feature is off.
- Removed: handling of batch rows saved without an id; no such rows exist.
2.1.0
- Tested with WooCommerce up to 11.2.
- Fixed: setting an order to Refunded now restocks only the units of refunds where "Restock refunded items" was ticked, the same as a partial refund. Cancelled and failed orders still put every unit back.
- Fixed: Markdown Earnings revenue is now net of refunds. The column, the summary and the CSV header read "Revenue recovered (net of refunds)".
- Improved: the expiry digest tracks delivery per recipient. An address that keeps failing no longer makes the other recipients get the same batches again, and the Email Log no longer fills with duplicates.
- Fixed: allergens inside an ingredient's sub-ingredients are printed bold too, as the EU food information rules ask.
- Added: the Store API lists a Variable food product's variations, prices and price range the same way as a Variable product's, and the Add to Cart with Options block shows the classic add-to-cart form for both food product types.
- Fixed: Variable food products match Variable products in more places. Duplicating a product copies its variations, renaming it renames them, switching between Variable product and Variable food product keeps them, new attributes are ticked "Used for variations", and variation attribute summaries are updated when attributes are saved.
- Fixed: a Variable food product without a chosen variation can no longer be added to the cart or to an order in wp-admin, and Bulk Edit and Quick Edit can change a Simple food product's price.
- Fixed: batch stock stays right in more cases. A reduced line takes only the units its batch still has, so a later refund cannot add units that were never there, with an order note naming any shortfall; checkout claims are re-read under the order lock; and, with WordPress posts order storage, orders trashed, restored or deleted from the orders list update their batches.
- Fixed: a product hidden from the catalogue for having nothing left to sell is shown again when it changes to a non-food type, and that state is refreshed when its markdown opt-out is saved and every night. The struck-through cart price follows the cart's tax display, and saving more discount steps than allowed shows an error instead of dropping the deepest ones.
- Fixed: category switching runs at local midnight, skips categories that were deleted, never skips or repeats a product, puts a product's categories back when it changes to a non-food type, and the Switching Log explains why each move was made.
- Fixed: CSV exports open correctly in Excel with accented letters, are named after the shop's own date, and write category names as shown. Markdown Earnings search finds accented names whatever their letter case, the filter keeps the column sort and works with non-Latin category names.
- Fixed: settings that would be changed or clamped on save are refused with an error naming the field, "Send one now" says when there is no recipient, and the Email Log shows dates in the plugin's date format.
- Fixed: in the product editor, a cleared nutrition table stays empty, ingredient percentages over 100 are refused with a notice, rearranging a variation's nutrition rows is saved, and a typed full stop at the end of the "may contain" text is no longer doubled.
- Improved: security hardening. Every request value is sanitized where it is read, help tips and prices are filtered through WooCommerce's and WordPress's allowed markup, database table names are passed as identifiers, and the Expiry Dashboard refuses exports and deletes while it is switched off.
- Improved: translations use whole sentences instead of joined fragments, correct plural forms in the admin scripts, numbers and percentages in the site's language, and list separators from WordPress.
- Improved: the Appearance preview uses current dates and the shop's price format, reads three-digit colour codes such as #abc, shows contrast ratios in the admin's language and has restyled intro and warning panels.
- Improved: accessibility and right-to-left layout: the Appearance preview's theme buttons report their state, colour swatches show a focus ring, the earnings expander names its rows, ingredient chips handle the keyboard correctly, and the nutrition table, nutrition editor and Appearance screen mirror for right-to-left languages.
- Improved: performance and packaging. Pages other than product pages skip the offer lookup, the nightly job no longer walks the catalogue while Automatic Markdown or Expiry & Tracking is off, each editor's script loads only while its feature is on, a Variable food product's variation data is read in one query, and the release zip leaves out development files.
1.4.2
- Fixed: the ingredient and product-type editors now load their translations. The script translation files shipped in 1.4.1 had the wrong file names, so both editors stayed in English.
1.4.1
- Added translations for 25 languages: Arabic, Chinese (Simplified and Traditional), Czech, Danish, Dutch, Finnish, French, German, Greek, Hungarian, Icelandic, Indonesian, Italian, Japanese, Korean, Norwegian (Bokmål), Polish, Portuguese (Brazil), Romanian, Russian, Spanish, Swedish, Turkish and Ukrainian.
- Translations ship as .mo and .l10n.php files for PHP and as JSON for the admin scripts, so the ingredient and product-type editors are translated too.
- Each language uses its own plural rules, so counts like "3 days" or "5 sub-ingredients" read correctly.
- The offer label tokens {date}, {days} and {percent} work the same in every language.
1.4.0
- Requires WooCommerce 10.0 or later. Earlier WooCommerce releases let block checkout give back a batch claim before payment; the plugin now warns in wp-admin when it finds one.
- Fixed: saving a product no longer undoes sales made while its edit screen was open. A batch that sold or moved in the meantime keeps its real count, and you are told.
- Fixed: an unpaid order's batch units are put back automatically after WooCommerce's Hold stock time.
- Fixed: turning Expiry & Tracking off hands stock back to WooCommerce in batches that resume if interrupted, and are finished before the plugin is deactivated.
- Fixed: an order can no longer take the same units twice when two payment notifications arrive together, and a stock update held up by a busy lock is retried instead of dropped.
- Fixed: on multisite, one site without WooCommerce no longer switches the plugin off across the whole network, and sites sharing a database server no longer wait on each other's locks.
- Fixed: the settings screens save on WooCommerce 10.0.
- "N in stock" now follows WooCommerce's Stock display format setting.
- The expiry digest log records recipients by user rather than by email address.
- Smaller fixes: the Expiry Dashboard deletes a batch by its id, duplicating a product never copies its batches, "Send one now" respects the Expiry & Tracking switch, uninstalling clears every scheduled job, and plural wording is correct in every language.
1.3.1
- Fixed batch expiry, category switching, digest retries and Markdown Earnings edge cases;
improved performance for larger catalogues and made Food Variable controls more reliable.
- Fixed: two shoppers checking out the last units at once could both be sold them. Batch stock
is now claimed while checkout can still refuse, in classic and block checkout alike.
- Fixed: switching batch tracking off, changing the product type or turning Expiry & Tracking
off now gives WooCommerce its stock management back, with the batch total as the quantity.
- Fixed: sold-out and written-off batches no longer set the best-before date shown in the shop.
Where automatic markdown can offer the stock, the date is shown on the offer instead.
- Fixed: saving settings no longer sends the expiry digest at once or moves its schedule.
- Fixed: removing the last email recipient or category from a setting now saves.
- Fixed: a negative batch quantity typed in the editor is saved as zero rather than as stock.
- Fixed: Mini Cart blocks outside the shop pages now style the batch and offer lines.
1.3.0
Correctness release. The headline fix is a race condition in how batch stock was written.
- Fixed: two customers checking out at the same time could both buy the last units of a batch.
Every batch movement - a sale, a refund, a write-off, a product save - read the batch table,
changed it in PHP and wrote it back, so the second write silently discarded the first one's
deduction and the batch sold past its physical stock. Since the batch table is the stock, the
product's sellable figure was wrong with it. All nine places that write a batch table now do so
under a per-product lock. On a database with no lock support the write still goes through and a
line is written to the error log, because failing to record a sale that has already been paid
for is the same oversell by another route.
- Changed: a refund only puts units back on the shelf when "Restock refunded items" is ticked.
Previously every refund restocked, even when the customer kept damaged goods. Refunds made
before this release are treated as restocking, so nothing already in your books moves.
- Fixed: automatic category switching ignored quantity, so a written-off batch kept a product in
the "expired" category every night however much fresh stock was behind it. It now looks only at
batches that still have units, as the settings screen always said it did.
- Fixed: re-saving a product wiped its write-off history, zeroing the "Written off" column in
Markdown Earnings.
- Fixed: weekly, 2-weekly and 3-weekly digests were never scheduled when the plugin was activated
- only when the settings page was next saved.
- Fixed: the digest emailed products that are no longer tracked, and batches with no units left.
- Fixed: a reduced price could show as the full price in the mini-cart and on block-based carts
until the next page load.
- Fixed: refunding, cancelling or trashing an order placed while Automatic Markdown was on did not
credit its units back once the feature was switched off. Deleting a refund, and trashing an
order outright, now re-count it too.
- Fixed: on shops entering prices excluding tax and showing them including it (or the reverse),
the batch picker showed a different "full price" than the one printed above it, and Markdown
Earnings added two different tax bases together.
- Fixed: in "hide" mode, products taken off the shop still used up their slot in the product grid,
so a page of twelve could render four.
- Fixed: a product switched away from a food type kept answering its stock from batches nobody
could edit any more.
- Fixed: dates and times in the Switching Log could disagree by a day, and the date filter could
miss entries from the day it was filtering on.
- Fixed: "Copy to a new draft" carried the original's category-switching state, so the copy began
life as expired.
- Fixed: a nearing-expiry category could be left on a product after it had passed through expired
and back out again.
- Fixed: opening a variable product marked every variation as changed before anything was edited.
- Fixed: a reduced cart line that dropped to full price, because its batch sold out while the
shopper browsed, became a second line for the same product on the next add to cart.
- Security: the recipient picker no longer returns other users' email addresses, and the markdown
unit cap is bounded.
- Quantities and packing dates are stored as numbers and dates rather than as whatever was typed.
- Documented in the FAQ: once a product's batch table takes over as its stock, WooCommerce's own
stock figure for it is not recoverable after uninstalling.
1.2.0
- New "Appearance" settings tab: thirteen colours covering the ingredient list, the nutrition
table, the batch picker, the "Reduced to clear" badge and the best-before line, with a live
preview on a light and a dark theme and a WCAG contrast check on the pairs that carry text.
Every field starts empty and empty keeps the shipped styling, so an untouched shop is served
no extra CSS at all and keeps following its theme.
- The plugin is now called "Food Products for WooCommerce" wherever its name is shown.
- Automatic Markdown now offers every batch that is close enough to its date, each at its own
step, instead of only the one closest to expiry. The shopper picks the date they want.
- The Expiry Dashboard's columns can be sorted by clicking their headings, and the CSV export
follows the same order.
- The product page stylesheets now inherit the theme's font, size and colours, so the nutrition
table, ingredient list, best-before line and batch picker read correctly on dark and light
themes alike without custom CSS.
- Stylesheets and scripts are only loaded on the pages that use them: a non-food product page
loads none of them, and the markdown stylesheet no longer loads on every page of the site.
1.1.0
- The batch table is now a tracked product's stock. Switching "Track expiry dates and batches" on
turns WooCommerce's own stock management off for that product or variation (and for the parent
of a variable product) and hides the option; the product is in stock while a batch in date has
units left, the product page shows the batch total as "N in stock", and a shopper cannot buy
more than the batches hold. A product that was managing stock shows what WooCommerce held, so
those units can be entered as batches.
- A full-price sale now comes off the freshest batch first, keeping the batch nearest its date for
the reduced offer. A reduced sale still comes off the batch the shopper picked.
- Stock past its best-before date is no longer sold at any price unless "When a batch passes its
best-before date" under Products is set to keeping it on sale, which adds a fixed last step to the
Automatic Markdown ladder for it with its own discount and wording. The ladder's own steps now
start at one day out.
- "When every batch has expired" is now "When nothing is left to sell", with two choices: show the
product as out of stock, or hide it. The old "keep selling at full price" choice no longer
applies and falls back to out of stock.
- What happens to a batch past its date is now one choice under Products - write it off by hand,
write it off nightly, or keep it on sale at a discount - rather than two toggles on two tabs that
ruled each other out.
- New Automatic Markdown: discount a batch automatically as its best-before date approaches,
on a ladder of steps you set, with a live preview of what a shopper would be offered, a cap on
how many units go on offer, a price floor, and a choice of what happens once every batch has
expired.
- New Markdown Earnings screen: what each marked-down batch sold, what it brought in, what
the discount cost, and how much expired unsold anyway.
- New Switching Log screen: every category move the nightly run makes, with the product, the
direction and the batch that caused it.
- New Ingredient list, with allergens picked out in bold, shown as its own tab on the
product page.
- New nightly write-off, and a write-off button on each batch, taking what is left of an expired
batch off WooCommerce stock so the shop stops selling it. Pick the hour it runs at, on your
shop's own clock; it starts at midnight. It never takes stock below zero.
- Batch quantities now follow every sale, not only the reduced ones: a sold unit comes off the
batch closest to its best-before date, the way the shelf is emptied, and a refund puts it back
on the batch it came off. Stock you have not put a date on is sold last. The Expiry Dashboard
and the batch table therefore read as what is still on the shelf rather than what was put there,
and an expiring batch is no longer written off stock that was already sold.
- Split the settings into a General tab and a new Products tab. Each dashboard is switched on
directly under the feature it reports on, and is only offered while that feature is on: Expiry
Dashboard and Switching Log on the Products tab, Markdown Earnings on the Automatic Markdown
tab. The General tab is left with the date format.
- The Markdown Earnings and Switching Log screens now open on the last year, and their date filters
are labelled From and To.
- The markdown plumbing an order line carries - the batch it came from and the amount saved - is no
longer listed on the order screen. The Best before date and the Offer it was sold under still are,
and the money is on the Markdown Earnings screen.
- Rewrote the option names, labels, tooltips and messages across every screen in plain English,
with one term per thing throughout.
- The write-off and delete buttons on the Expiry Dashboard now name themselves on hover, so an
icon-only action says what it does before it is clicked.
- Every switch in the Expiry & Tracking tab now carries a help tip saying what it does, on both a
Simple food product and each variation of a Variable food product.
- The Best before date and the Offer a reduced line was bought under now sit on a line each in the
basket, mini-basket and checkout, instead of running together into one sentence.
- Fixed a Variable food product adding its parent to the cart instead of the variation the shopper
picked. WooCommerce chooses how to handle an add-to-cart by product type and only knows its own,
so the chosen variation was being dropped - which also made a marked-down variation come back as
"the reduced units have just sold out", the batch having been looked for on the parent.
- Nutrient table rows can be dragged by their grip again - jQuery UI was cancelling every drag
because the grip is a button.
- Fixed the "Declared per" choices stacking on top of each other in the product-level Nutrient
tab, so 100 g and 100 ml can be picked again.
- Dropped "Serving" as a declaration basis. EU FIC 1169/2011 wants the declaration itself per
100 g or per 100 ml, so a serving size is now simply printed alongside whichever of those is
chosen, on both the product page and per variation. Products that had saved "Serving" fall
back to per 100 g and keep showing their serving size.
- Fixed the Unit select being taller than the fields beside it, in both the product tab and the
variation panels.
- Fixed the nutrient row buttons sitting top-left instead of centred inside variation panels.
- Removed the nutrient row counter and the customer preview from the editor.
- Nutrient tables inside variation panels are drag-sortable again. The handler that set them up
listened on the wrong element for WooCommerce's "variations loaded" event, so it never ran.
1.0.2
- Renamed the plugin display name to match the assigned plugin slug, as asked for in the plugin
review. Nothing you have set up changes.
- Renamed the plugin slug and text domain from
food-products-for-woocommerce to
mfood-products-for-woocommerce, as asked for in the plugin review. The plugin name, its
settings and all data saved on your products are unchanged.
1.0.1
- Prefixed every remaining global name (options, meta, hooks, cron schedule keys, script
handles, page slugs, CSS classes) and removed the dynamic prefix concatenation.
- Fixed an unbalanced closing tag on the settings screens.
- Guarded product category lookups against an error result.
- Guarded a missing quantity value when building the notification email.
- Batched the email log writes into a single option update.
- Passed fputcsv() its delimiter, enclosure and escape explicitly, for PHP 8.4.
- Removed unused code and corrected documentation.
1.0.0