WordPress plugin that extends WooCommerce with a dedicated workflow for managing print orders sent to third-party print providers. It adds custom order statuses, tracking fields, CSV export, protocol-based CSV imports, and search/filter capabilities tailored to a print fulfillment pipeline.
studiou-wc-ord-print-statuses| Component | Minimum | Tested up to |
|---|---|---|
| WordPress | 5.8 | 6.9.4 |
| WooCommerce | 3.0 | 10.7.0 |
| PHP | 7.2 | — |
Compatible with WooCommerce High-Performance Order Storage (HPOS) — the plugin declares custom_order_tables compatibility and queries the HPOS wc_orders table directly.
studiou-wc-ord-print-statuses folder into wp-content/plugins/.No additional setup is required — custom statuses, fields, columns, and bulk actions are registered on activation.
Two new statuses are inserted in the WooCommerce status pipeline, right after processing:
| Slug | Label | Purpose |
|---|---|---|
wc-to-print |
Prepare to Printing | Order is queued and exported to the print shop. |
wc-in-print |
In Printing | Print shop has accepted the order. |
Status transitions automatically stamp the related metadata field (to_print_date / in_print_date) with the current server time.
A new Print Information panel is added to the order details screen with four editable fields:
to_print_date)in_print_date)external_ref_ord_no)external_ref_ord_date) — populated by InPrint protocol imports.external_ref_ord_delivered)Available in the Bulk actions dropdown on WooCommerce → Orders:
wc-to-print.wc-to-print and stamps to_print_date.wc-in-print and stamps in_print_date.Three columns are appended after Total:
The standard WooCommerce Search orders input also matches the external_ref_ord_no meta value — no separate input is required. Works under both legacy CPT storage and HPOS (woocommerce_shop_order_search_fields and woocommerce_order_table_search_query_meta_keys filters).
A WooCommerce → Import Protocols submenu exposes two CSV upload forms:
wc-in-print and the external reference number / date are saved.wc-completed and the delivery timestamp is recorded.Both importers require the manage_woocommerce capability and are protected by WordPress nonces.
About
order_no: theorder_nocolumn written to the export and read by both imports is the WooCommerce internal order ID (wp_wc_orders.idunder HPOS,wp_posts.IDunder legacy CPT). It is not the user-facing order number produced by Sequential Order Numbers and similar plugins. If you install a custom-order-number plugin, the protocols still operate on the underlying order IDs — the print shop must echo back whatever value was sent in the export.
Generated file: prepare_to_printing_export.csv (UTF-8 with BOM — opens correctly in Excel on Windows).
Columns (in order):
order_no, prod_cat, prod_name, prod_var, prod_var_type, prod_img_url, qty, email
prod_var_type is sourced from the variation's attribute_pa_format postmeta, resolved against the wp_terms table.prod_cat is a comma-separated list (deduplicated) when a product belongs to multiple product_cat terms — the row itself is not duplicated.Headers are normalised on read: column names are lowercased and internal whitespace is collapsed to underscore. Both the original spec casing (Order No, ExternalOrder, ExternalOrderDate) and the documented lowercase form (order_no, externalorder, externalorderdate) work. Rows are also deduplicated by order_no before processing.
Required headers (any of the accepted casings):
order_no, externalorder, externalorderdate
Effect per row: order → status wc-in-print, sets in_print_date (now), external_ref_ord_no, external_ref_ord_date (parsed from the CSV). Orders in a terminal status (completed, cancelled, refunded, failed) are skipped with a per-row error notice.
Required header:
order_no
Effect per row: order → status wc-completed, sets external_ref_ord_delivered (now).
The plugin follows a manager-based pattern. The main bootstrapper (studiou-wc-ord-print-statuses.php) wires up specialised classes, each owning one slice of behaviour:
| Class | File | Responsibility |
|---|---|---|
Order_Status_Manager |
includes/class-order-status-manager.php |
Register custom statuses, handle transitions. |
Order_Fields_Manager |
includes/class-order-fields-manager.php |
Render and persist Print Information fields. |
Bulk_Actions_Manager |
includes/class-bulk-actions-manager.php |
Register and dispatch bulk actions. |
Custom_Columns_Manager |
includes/class-custom-columns-manager.php |
Inject extra columns into the Orders list. |
Import_Manager |
includes/class-import-manager.php |
Admin UI + handlers for protocol CSV imports. |
Order_Search_Manager |
includes/class-order-search-manager.php |
Extend order search to external reference no. |
Studiou_DB_Manager |
includes/class-db-manager.php |
All SQL and protocol-processing logic. |
UtilsLog |
includes/utils-log.php |
Lightweight logging helper (uses WP_DEBUG). |
The export query in Studiou_DB_Manager::get_orders_for_printing() joins wc_orders, wc_order_product_lookup, posts (products + variations), postmeta (variation attribute + thumbnail), terms, term_taxonomy, and term_relationships to assemble the print-shop payload in a single round-trip.
studiou-wc-ord-print-statuseslanguages/cs_CZ) — see docs/translations.txt for the EN→CS reference list.Use the standard WordPress functions (__(), _e(), _x(), _n_noop()) for any new strings.
UtilsLog::log($message) writes to the WordPress error log when WP_DEBUG is enabled (guarded as defined('WP_DEBUG') && WP_DEBUG — works for both boolean and integer truthy values). With WP_DEBUG_LOG on, log output ends up in wp-content/debug.log.UtilsLog::message($message, $type) queues a dismissible WordPress admin notice (info / success / warning / error) for the current user. Notices are stored in a per-user transient (60-second TTL) so they survive the redirect that follows bulk actions and imports. Used to report success counts, skipped orders, malformed CSV rows, etc.studiou-wc-ord-print-statuses/
├── studiou-wc-ord-print-statuses.php # Plugin bootstrap (header + main class)
├── includes/
│ ├── class-db-manager.php
│ ├── class-order-status-manager.php
│ ├── class-order-fields-manager.php
│ ├── class-order-search-manager.php
│ ├── class-bulk-actions-manager.php
│ ├── class-custom-columns-manager.php
│ ├── class-import-manager.php
│ └── utils-log.php
├── languages/ # .po / .mo translation files
├── docs/ # Project documentation (see below)
├── README.md # This file
└── CLAUDE.md # Guidance for Claude Code sessions
Reference documents live under docs/:
docs/readme.md — Previous user-facing readme (kept for history).docs/instructions.txt — Original feature specification used to scaffold the plugin.docs/translations.txt — EN → CS translation reference.docs/revise-1.3.2.md — Analytical review of v1.3.2 (all findings addressed in 1.4.0).docs/revise-1.4.0.md — Analytical review of v1.4.0 (all in-scope findings addressed in 1.4.1).docs/revise-1.4.1.md — Analytical review of v1.4.1 (all in-scope findings addressed in 1.4.2).Project-internal guidance for AI-assisted development is documented in CLAUDE.md.
woocommerce_order_table_search_query_meta_keys) has not been live-verified against WC 10.7.0. The legacy CPT filter is solid; the HPOS branch should be smoke-tested on the production site before relying on it.prod_var / prod_var_type. Simple (non-variable) products appear in the export as of 1.4.2 (via LEFT JOIN), but their variation fields are blank.wc_order_product_lookup, the WooCommerce Analytics lookup table. If WC Analytics is disabled or the table is missing, the export emits an admin notice and skips.woocommerce_admin_order_data_after_order_details. Behaviour on the WC 10.x block-based order edit screen has not been verified.Network: plugin header is set; activate per blog.to_print_date, external_ref_ord_no, etc.) remain in the database after plugin removal.attribute_pa_format taxonomy is hardcoded in the export SQL. Stores using a different attribute name will get a blank prod_var_type.group_concat_max_len is raised to 64 KB via SET SESSION per export — connection-scoped, harmless but worth knowing if you maintain custom queries on the same connection.Bug-fix release addressing the findings catalogued in docs/revise-1.4.1.md:
strtotime() interpreted the stored value in PHP server timezone, then date_i18n() formatted in WP timezone. On UTC-server / non-UTC-WP setups (e.g. UTC server + Europe/Prague site) the displayed time drifted by 1–2 hours. Now parsed explicitly as WP-local via DateTimeImmutable(..., wp_timezone()) and formatted with wp_date().wp_terms.name against the variation's attribute_pa_format postmeta, but WC stores the slug there, not the name. The join is now terms.slug = meta_value, while MIN(terms.name) still returns the human-readable display value.qty is now SUM(order_products.product_qty) instead of MIN(...). A single order can have multiple line items for the same (product, variation) when the customer added it twice without cart-merge; the previous query silently undercounted in that case.JOIN product_variations was inner — silently excluded orders containing simple (non-variable) products. Now a LEFT JOIN; simple products are included with blank prod_var/prod_var_type.prod_img_url no longer reads wp_posts.guid (which WP codex warns is not necessarily a URL). The export now selects _thumbnail_id and resolves URLs via wp_get_attachment_url() — respects offloaded-media plugins, multisite uploads paths, and wp_get_attachment_url filters.Studiou_DB_Manager::normalise_csv_date() now formats with wp_date() (WP timezone) instead of date() (server timezone), matching current_time('mysql') elsewhere.wc_order_product_lookup — if WC Analytics is disabled and the lookup table is missing, the export bails with a clear admin notice instead of failing with a cryptic SQL error.SET SESSION group_concat_max_len = GREATEST(@@SESSION.group_concat_max_len, 65535) — never lowers a higher session-scoped value another piece of code may rely on..csv extension check on protocol uploads (defense in depth on top of the existing is_uploaded_file() / $_FILES[...]['error'] checks).Order_Status_Manager::add_custom_order_statuses_to_list() falls back to appending the custom statuses at the end of the dropdown if wc-processing was removed by another plugin.parse_csv() drops error-suppression @fopen — failures are logged explicitly.order_no means in CSVs. CLAUDE.md corrected (the attribute_pa_format join uses slug, not name).docs/revise-1.4.1.md updated with per-item status annotations.Deferred (carried over): live HPOS search-filter verification,
Network:plugin header,uninstall.php, block-based order edit screen, custom-order-number plugin integration.
Bug-fix release addressing the findings catalogued in docs/revise-1.4.0.md:
ONLY_FULL_GROUP_BY mode (default in MySQL 5.7+ / MariaDB 10.3+). Non-aggregated columns (prod_name, prod_var, prod_var_type, prod_img_url, qty, email) are wrapped in MIN(); each (product_id, variation_id) group has exactly one value per column so MIN() is value-preserving.parse_csv() now strips a leading UTF-8 BOM. Re-importing a previously exported CSV (which ships with a BOM for Excel-on-Windows compatibility) no longer breaks header parsing.YYYY-MM-DD HH:MM:SS ↔ YYYY-MM-DDTHH:MM) instead of running them through strtotime()/date(). The previous timezone-shift bug (off by 1–2 hours on UTC servers with non-UTC WP timezones) is gone.set_order_processing_status() now records a single order note via update_status() instead of also calling add_order_note(). The order activity feed no longer duplicates the "payment received" line.output_csv() now aborts with wp_die() (and logs the offender) when headers_sent() is true, instead of streaming raw CSV bytes into a half-rendered HTML page.fputcsv/fgetcsv now pass an explicit empty-string escape character — no more PHP 8.4 deprecation warnings.Order_Fields_Manager::save_custom_order_fields() now checks current_user_can('edit_shop_order' / 'edit_shop_orders') before writing. Defense-in-depth on top of WC's own validation.Studiou_DB_Manager::get_orders_for_printing() raises group_concat_max_len to 64 KB before the export query, preventing silent prod_cat truncation for products in many categories.dedupe_by_order_no() is now case-insensitive — ord-12345 and ORD-12345 collapse to a single row.normalise_csv_date() returns an empty string (and logs) on parse failure instead of persisting the unparseable raw value.Import_Manager::run_import() is driven by a single protocol_dispatch() map (file field, nonce, DB handler per protocol). Every non-happy-path branch returns explicitly via redirect_back() for static-analysis friendliness.Order_Fields_Manager rendering and saving are driven from the datetime_fields() / text_fields() maps. Adding a new field is a single-map-entry change.docs/revise-1.4.0.md updated with per-item status annotations.Deliberately deferred (carried over from the 1.4.0 review): live verification of the HPOS search filter name (
woocommerce_order_table_search_query_meta_keys),Network:plugin header for multisite,uninstall.phpcleanup, and behaviour on the WC 10.x block-based order edit screen. None of these were addressed by 1.4.1.
Resolution of every finding catalogued in docs/revise-1.3.2.md. Highlights:
external_ref_ord_no consistently, no _ prefix), and the integration uses WooCommerce's own search filters (woocommerce_shop_order_search_fields for legacy CPT and woocommerce_order_table_search_query_meta_keys for HPOS). No separate UI input — typing the external order number into the standard Search orders box finds the order.Order_Status_Manager::handle_status_transitions() now writes timestamps via wc_get_order()->update_meta_data() instead of update_post_meta(). Print-date stamps are no longer silently lost under HPOS for non-bulk status transitions.order_no (per the original spec) and accept both the spec's title-case headers and the lowercase form documented in the README.charset=utf-8 header + leading \xEF\xBB\xBF). Czech diacritics render correctly in Excel on Windows.product_cat terms — categories are concatenated via GROUP_CONCAT(DISTINCT …) inside a GROUP BY per line item.$wpdb->prepare() with %d placeholders rather than relying on intval() for safety.woocommerce_payment_complete events no longer demote orders already in to-print or in-print back to processing. The guard extends to all print statuses plus completed.current_user_can('edit_shop_orders'). Imports keep their existing manage_woocommerce guard.completed, cancelled, refunded, failed) instead of silently reactivating them.UtilsLog::log() guard is now defined('WP_DEBUG') && WP_DEBUG (previously true === WP_DEBUG, which failed when WP_DEBUG was defined as integer 1).UtilsLog::message() is now implemented as a per-user transient that renders via the admin_notices hook. Bulk actions and imports now produce visible user feedback after the redirect.parse_csv() no longer truncates rows at 1000 bytes (passes 0 to fgetcsv), validates that the header row is non-empty, normalises header casing, and skips rows whose column count diverges from the header.$_FILES[...]['error'] and is_uploaded_file() server-side instead of relying solely on the client-side accept=".csv" attribute.external_ref_ord_date is parsed via strtotime() and stored in MySQL datetime format; if parsing fails the raw value is retained.external_ref_ord_date is now rendered in the Print Information panel on the order edit screen.STUDIOU_WC_OPS_VERSION and STUDIOU_WC_OPS_FILE constants for code-side version references.plugins_loaded priorities (5 and 10) so translations are guaranteed to load before any manager instantiates.// visualized leftover comments and the dead-code commented body of UtilsLog::message().CLAUDE.md updated, docs/translations.txt extended with the strings that were missing.Translations: the
.pofile ships new strings; the.mofile must be rebuilt (e.g. viawp i18n make-mo languages/or Poedit) before the new Czech wording is visible. This release does not regenerate the binary.
prod_var_type column was empty in prepare_to_printing_export.csv. Variation attribute is now resolved through postmeta.attribute_pa_format joined against wp_terms.name.custom_order_tables compatibility.For bug reports, feature requests or commercial support, contact quadarax.com.
Released under the GNU General Public License v2.0 or later — see https://www.gnu.org/licenses/gpl-2.0.html.