# QDR — Studiou WC Order Print Statuses 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. - **Plugin slug / text domain:** `studiou-wc-ord-print-statuses` - **Current version:** 1.4.4 - **Author:** Dalibor Votruba — [quadarax.com](https://www.quadarax.com) - **License:** GPL v2 or later --- ## Requirements | 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. --- ## Installation 1. Copy the `studiou-wc-ord-print-statuses` folder into `wp-content/plugins/`. 2. Make sure WooCommerce is installed and active. 3. Activate the plugin from **Plugins** in the WordPress admin. No additional setup is required — custom statuses, fields, columns, and bulk actions are registered on activation. --- ## Features ### 1. Custom order statuses 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. ### 2. Print Information fields on the order edit page A new **Print Information** panel is added to the order details screen with four editable fields: - **Prepare To Print Date** (`to_print_date`) - **In Print Date** (`in_print_date`) - **External Order Number** (`external_ref_ord_no`) - **External Order Date** (`external_ref_ord_date`) — populated by InPrint protocol imports. - **External Order Delivered** (`external_ref_ord_delivered`) ### 3. Bulk actions on the Orders list Available in the **Bulk actions** dropdown on **WooCommerce → Orders**: - **Prepare to Printing Export** — runs the print-shop export SQL across the selected orders, streams the result as a CSV download, and moves every selected order to `wc-to-print`. - **Set Status to Prepare to Printing** — moves selected orders to `wc-to-print` and stamps `to_print_date`. - **Set Status to In Printing** — moves selected orders to `wc-in-print` and stamps `in_print_date`. ### 4. Extra columns in the Orders list Three columns are appended after **Total**: - **To Print Date** - **In Print Date** - **External Order Number** ### 5. Search by External Order Number 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). ### 6. Protocol imports A **WooCommerce → Import Protocols** submenu exposes two CSV upload forms: - **Import InPrint Protocol** — for every row in the file, the matching order is set to `wc-in-print` and the external reference number / date are saved. - **Import Delivered Protocol** — for every row in the file, the matching order is set to `wc-completed` and the delivery timestamp is recorded. Both importers require the `manage_woocommerce` capability and are protected by WordPress nonces. --- ## CSV formats > **About `order_no`:** the `order_no` column written to the export and read by both imports is the **WooCommerce internal order ID** (`wp_wc_orders.id` under HPOS, `wp_posts.ID` under 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. ### Export — Prepare to Printing 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. - The file is generated **before** any status change. If the underlying SQL returns no rows the file is not produced and statuses are still advanced (per spec), with an admin notice surfacing the situation. 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. ### Import — InPrint Protocol 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. ### Import — Delivered Protocol Required header: ``` order_no ``` Effect per row: order → status `wc-completed`, sets `external_ref_ord_delivered` (now). --- ## Architecture 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. --- ## Internationalization - Text domain: `studiou-wc-ord-print-statuses` - Translation files: `languages/` - Bundled locales: **Czech (`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. --- ## Logging & user notices - `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. --- ## Project layout ``` 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 ``` --- ## Documentation Reference documents live under [`docs/`](docs/): - [`docs/readme.md`](docs/readme.md) — Previous user-facing readme (kept for history). - [`docs/instructions.txt`](docs/instructions.txt) — Original feature specification used to scaffold the plugin. - [`docs/translations.txt`](docs/translations.txt) — EN → CS translation reference. - [`docs/revise-1.3.2.md`](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`](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`](docs/revise-1.4.1.md) — Analytical review of v1.4.1 (all in-scope findings addressed in 1.4.2). - [`docs/revise-1.4.2.md`](docs/revise-1.4.2.md) — Fresh-eye analytical review of v1.4.2 (all in-scope findings addressed in 1.4.3). - [`docs/revise-1.4.3.md`](docs/revise-1.4.3.md) — Fresh-eye analytical review of v1.4.3 (all in-scope findings addressed in 1.4.4). Project-internal guidance for AI-assisted development is documented in [`CLAUDE.md`](CLAUDE.md). --- ## Known limitations - **HPOS search filter name** (`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. - **Variable products only** — the export query joins variation tables and the CSV columns include `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 Analytics dependency** — the export reads from `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. - **Block-based order edit screen** — the Print Information panel is wired to the classic order edit screen via `woocommerce_admin_order_data_after_order_details`. Behaviour on the WC 10.x block-based order edit screen has not been verified. - **Multisite** — no `Network:` plugin header is set; activate per blog. - **No uninstall cleanup** — meta keys (`to_print_date`, `external_ref_ord_no`, etc.) remain in the database after plugin removal. - **Custom order numbers** — the protocols operate on WC internal order IDs (see the note above the Export section). Sequential Order Numbers / custom-order-number plugins are not integrated. - **The `attribute_pa_format` taxonomy is hardcoded** in the export SQL. Stores using a different attribute name will get a blank `prod_var_type`. - **MySQL `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. - **Excel CS-locale CSV separator** — the export uses `,` as the field separator. Czech Excel defaults to `;` as the list separator and may render the file as one column. Workaround: open with the Text Import wizard or set Windows regional list-separator to `,`. Other consumers (LibreOffice Calc, Numbers, programmatic CSV parsers) handle the comma natively. - **Trashed/draft products in historical orders** still appear in the export. Order line items reference product IDs; the export joins by ID without filtering on `post_status`. The intended behaviour: show what was sold, even if the product has since been removed from the catalogue. ## Changelog ### 1.4.4 — 2026-05-12 Bug-fix release addressing the findings catalogued in [`docs/revise-1.4.3.md`](docs/revise-1.4.3.md): - **Fixed (high):** `Studiou_DB_Manager::normalise_csv_date()` was shifting timezone-less CSV dates by the WP timezone offset. WordPress sets the PHP timezone to UTC very early, so `strtotime("2026-05-12 14:30")` returned a UTC timestamp; `wp_date()` then formatted it in WP-local, producing a Prague-shifted "2026-05-12 16:30:00". Now uses pure-regex reformat for ISO-like inputs (no shift) and `DateTimeImmutable(..., wp_timezone())->setTimezone(wp_timezone())` for the fallback path (handles explicit-timezone inputs correctly). **No remaining `strtotime` calls in `includes/`** — the recurring timezone-bug pattern is now structurally eliminated. - **Fixed:** `Studiou_DB_Manager::get_orders_for_printing()` now pre-checks the HPOS `wc_orders` table parallel to the existing `wc_order_product_lookup` check. If HPOS is disabled the export bails with a clear admin notice ("Export requires WooCommerce HPOS … Enable High-Performance Order Storage …") instead of failing with a cryptic SQL error. - **Fixed:** `bulk_set_print_status` now passes an explanatory note to `update_status` — "Marked Prepare to Printing by bulk action." / "Marked In Printing by bulk action." — matching the audit-trail pattern the import paths use. - **Changed:** `Custom_Columns_Manager` uses the `WC_Order` object directly when WC hands one to the column-content callback (HPOS path); under legacy CPT it memoises by ID per request so the same order isn't re-resolved across the three custom columns. Avoids 100–150 `wc_get_order` calls per 50-orders-per-page list. - **Changed:** `Import_Manager::emit_import_summary()` suppresses the "0 orders updated successfully" notice when no rows changed — the per-error notice stands on its own. Matches the `notify_moved_count` pattern. - **Changed:** `call_user_func($cfg['handler'], $csv_data)` → direct callable invocation. Tighter and slightly faster. - **Changed:** The `before_woocommerce_init` HPOS-compat declaration is now a named function (`studiou_wc_ord_print_statuses_declare_hpos_compat`). Can be `remove_action`-ed from outside. - **Doc:** [`docs/revise-1.4.3.md`](docs/revise-1.4.3.md) updated with per-item status annotations. ### 1.4.3 — 2026-05-12 Bug-fix release addressing the findings catalogued in [`docs/revise-1.4.2.md`](docs/revise-1.4.2.md): - **Fixed (high):** `import_delivered_protocol` was missing the terminal-status guard that `import_inprint_protocol` had. A `cancelled`, `refunded`, or `failed` order in the delivered CSV was silently reactivated to `completed`. Now skipped with a per-row error notice. `completed` orders are still allowed through (re-running the same protocol is a benign no-op transition). - **Fixed (high):** Import Protocols screen required `manage_woocommerce` (Administrator-only) while bulk actions only required `edit_shop_orders` (Shop Manager). Shop Managers can now run protocol imports — the two paths share the same capability requirement. - **Fixed:** `Studiou_DB_Manager::table_exists()` wraps the table name in `$wpdb->esc_like()`. The previous `SHOW TABLES LIKE %s` left the conventional `_` in `$wpdb->prefix` as a SQL wildcard; the post-query equality comparison mitigated false positives but the safety relied on the wrong layer. - **Fixed:** CSV export now defuses Excel/LibreOffice/Sheets formula injection — any cell beginning with `=`, `+`, `-`, `@`, `\t`, or `\r` is prefixed with a single quote. OWASP-recommended pattern; the leading quote is hidden by spreadsheet apps but disables formula evaluation. - **Fixed:** `Order_Fields_Manager::datetime_local_to_mysql()` now validates date and time ranges via `checkdate()` plus hour/minute/second bounds. Direct POST or DevTools-edited form values like `2026-13-45T25:99` no longer slip through to be stored as bogus MySQL datetimes. - **Fixed:** `qty` is now formatted in PHP — `SUM(DECIMAL)` returns `"2.0000"` from MySQL; the post-processing step normalises whole numbers to `"2"` and trims trailing zeros from decimals. - **Fixed:** Both import handlers now pass an explanatory note to `update_status` ("Marked In Printing by InPrint Protocol import." / "Marked completed by Delivered Protocol import."). The order activity feed reflects how the transition was triggered. - **Fixed:** Both import handlers now log per-row to `UtilsLog::log` — symmetric with the bulk-action path. Helps debug failed protocol imports. - **Fixed:** `Import_Manager::emit_import_summary()` deduplicates repeated error strings. A CSV with 100 malformed rows shows "100 errors occurred: Invalid row format in CSV." once, not 100 times concatenated. - **Changed:** `UtilsLog::render_notices()` switched from `wp_kses_post()` to `esc_html()` — current notice messages are plain text, the tighter escape narrows the trust boundary at the API edge. - **Removed:** `Order_Status_Manager::set_order_processing_status()` and its `woocommerce_payment_complete` hook. On modern WC the handler was effectively dead code — `WC_Order::payment_complete()` sets the order to `processing` (or `completed` for downloadable-only) *before* firing the action, so our handler was either a no-op same-state update or a deliberate skip. The corresponding translation string was removed too. - **Doc:** README "Known limitations" gains two entries (Excel CS-locale CSV separator, trashed/draft products in historical orders). CLAUDE.md picks up the 1.4.3 changes. - **Doc:** [`docs/revise-1.4.2.md`](docs/revise-1.4.2.md) updated with per-item status annotations. > **Deferred** (carried over from earlier reviews): live HPOS search-filter verification, block-based order edit screen, `Network:` plugin header, `uninstall.php`, custom-order-number plugin integration. ### 1.4.2 — 2026-05-12 Bug-fix release addressing the findings catalogued in [`docs/revise-1.4.1.md`](docs/revise-1.4.1.md): - **Fixed (high):** Custom_Columns_Manager date columns ("To Print Date", "In Print Date") were applying the WP timezone offset twice — `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()`. - **Fixed (high):** Export SQL joined `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. - **Fixed (high):** `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. - **Fixed:** `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`. - **Fixed:** `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. - **Fixed:** `Studiou_DB_Manager::normalise_csv_date()` now formats with `wp_date()` (WP timezone) instead of `date()` (server timezone), matching `current_time('mysql')` elsewhere. - **Added:** Pre-flight check for `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. - **Changed:** `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. - **Added:** Server-side `.csv` extension check on protocol uploads (defense in depth on top of the existing `is_uploaded_file()` / `$_FILES[...]['error']` checks). - **Fixed:** `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. - **Changed:** `parse_csv()` drops error-suppression `@fopen` — failures are logged explicitly. - **Doc:** README gains a "Known limitations" section and clarifies what `order_no` means in CSVs. CLAUDE.md corrected (the `attribute_pa_format` join uses **slug**, not name). - **Doc:** [`docs/revise-1.4.1.md`](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. ### 1.4.1 — 2026-05-12 Bug-fix release addressing the findings catalogued in [`docs/revise-1.4.0.md`](docs/revise-1.4.0.md): - **Fixed (critical):** Export SQL is now compatible with MySQL's `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. - **Fixed (critical):** `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. - **Fixed (high):** Datetime fields in the Print Information panel now reformat values as pure strings (`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. - **Fixed:** `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. - **Fixed:** Bulk-action notices are suppressed when zero orders were actually moved — the more informative "skipped because terminal status" warning from the DB manager stands alone. - **Fixed:** `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. - **Fixed:** `fputcsv`/`fgetcsv` now pass an explicit empty-string escape character — no more PHP 8.4 deprecation warnings. - **Fixed:** `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. - **Fixed:** `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. - **Fixed:** `dedupe_by_order_no()` is now case-insensitive — `ord-12345` and `ORD-12345` collapse to a single row. - **Fixed:** `normalise_csv_date()` returns an empty string (and logs) on parse failure instead of persisting the unparseable raw value. - **Changed:** `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. - **Changed:** `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. - **Changed:** Notice transient TTL raised from 60 s to 300 s so admin notices survive slow redirects (proxies, HTTPS handshake latency). - **Doc:** [`docs/revise-1.4.0.md`](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.php` cleanup, and behaviour on the WC 10.x block-based order edit screen. None of these were addressed by 1.4.1. ### 1.4.0 — 2026-05-12 - **Tested with:** WordPress 6.9.4, WooCommerce 10.7.0 (HPOS enabled). Resolution of every finding catalogued in [`docs/revise-1.3.2.md`](docs/revise-1.3.2.md). Highlights: - **Fixed:** Search by External Order Number is now functional. The meta-key mismatch is resolved (`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. - **Fixed:** `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. - **Fixed:** CSV imports now deduplicate rows by `order_no` (per the original spec) and accept both the spec's title-case headers and the lowercase form documented in the README. - **Fixed:** "Prepare to Printing Export" is now atomic — the CSV is generated in memory before any status change. Order status is advanced after a successful CSV build. If the export query returns no rows the user is notified. - **Fixed:** Export CSV is now UTF-8 with BOM (`charset=utf-8` header + leading `\xEF\xBB\xBF`). Czech diacritics render correctly in Excel on Windows. - **Fixed:** Export SQL no longer multiplies rows when a product belongs to multiple `product_cat` terms — categories are concatenated via `GROUP_CONCAT(DISTINCT …)` inside a `GROUP BY` per line item. - **Fixed:** Export SQL now goes through `$wpdb->prepare()` with `%d` placeholders rather than relying on `intval()` for safety. - **Fixed:** Late `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`. - **Fixed:** Bulk actions guard with `current_user_can('edit_shop_orders')`. Imports keep their existing `manage_woocommerce` guard. - **Fixed:** Bulk actions and protocol imports skip orders in terminal statuses (`completed`, `cancelled`, `refunded`, `failed`) instead of silently reactivating them. - **Fixed:** `UtilsLog::log()` guard is now `defined('WP_DEBUG') && WP_DEBUG` (previously `true === WP_DEBUG`, which failed when `WP_DEBUG` was defined as integer `1`). - **Fixed:** `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. - **Fixed:** `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. - **Fixed:** Import handlers verify `$_FILES[...]['error']` and `is_uploaded_file()` server-side instead of relying solely on the client-side `accept=".csv"` attribute. - **Fixed:** `external_ref_ord_date` is parsed via `strtotime()` and stored in MySQL datetime format; if parsing fails the raw value is retained. - **Added:** `external_ref_ord_date` is now rendered in the Print Information panel on the order edit screen. - **Added:** `STUDIOU_WC_OPS_VERSION` and `STUDIOU_WC_OPS_FILE` constants for code-side version references. - **Changed:** Translation loader and plugin bootstrap now have explicit `plugins_loaded` priorities (5 and 10) so translations are guaranteed to load before any manager instantiates. - **Removed:** `// visualized` leftover comments and the dead-code commented body of `UtilsLog::message()`. - **Docs:** README architecture/install/usage updated, `CLAUDE.md` updated, `docs/translations.txt` extended with the strings that were missing. > **Translations:** the `.po` file ships new strings; the `.mo` file must be rebuilt (e.g. via `wp i18n make-mo languages/` or Poedit) before the new Czech wording is visible. This release does not regenerate the binary. ### 1.3.2 — 2025-10-18 - **Fixed:** `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`. - **Fixed:** HPOS incompatibility warning. Plugin now explicitly declares `custom_order_tables` compatibility. - **Tested:** WordPress 6.8.3, WooCommerce 10.2.2 with HPOS enabled. ### 1.3.1 - Previous stable release. --- ## Support For bug reports, feature requests or commercial support, contact **[quadarax.com](https://www.quadarax.com)**. --- ## License Released under the **GNU General Public License v2.0 or later** — see [https://www.gnu.org/licenses/gpl-2.0.html](https://www.gnu.org/licenses/gpl-2.0.html).