Skip to content

Releases: PaulArgoud/WP4Odoo

3.9.1

23 Feb 11:39

Choose a tag to compare

Fixed

  • WP Crowdfunding compatibility — Updated module detection from wpneo_crowdfunding_init() function to WPCF_VERSION constant, meta keys from _wpneo_* to _nf_* prefix, and version constant from STARTER_VERSION to WPCF_VERSION, matching WP Crowdfunding 2.x API surface
  • Unescaped admin outputs — Fixed 4 unescaped outputs in admin views (tab-health.php, tab-modules.php, partial-checklist.php)
  • PHPDoc non-generic arrays — Fixed 5 @return array@return array<string, mixed> in EDD/WC handler methods

Changed

  • Marketplace base classes — Extracted shared logic from Dokan, WCFM, and WC Vendors into Marketplace_Module_Base and Marketplace_Handler_Base, eliminating ~477 lines of duplicated code across 6 module/handler files
  • LMS handler template method — Added load_course() template method to LMS_Handler_Base with 3 abstract hooks (get_course_post_type(), get_course_price(), get_lms_label()), eliminating identical implementations from 5 LMS handlers (~80 lines)
  • LMS module shared enrollment — Moved identical load_enrollment_data() from 5 LMS modules into LMS_Module_Base via get_lms_handler() abstract (~35 lines)
  • Test stub consolidation — Consolidated 14 single-constant stub files into constants-only.php and removed 4 empty stubs, reducing bootstrap.php requires from 68 to 50
  • PHPDoc generics — Added array<string, mixed> return types to 11 methods in Module_Base and Settings_Repository
  • Events handler base class — Extracted shared format_event(), parse_event_from_odoo(), and format_attendance() from 3 event handlers into Events_Handler_Base abstract base
  • Booking handler base extendedJet_Booking_Handler and Jet_Appointments_Handler now extend Booking_Handler_Base (previously standalone), bringing handler base coverage to 5 booking handlers
  • Events test base — New EventsModuleTestBase abstract test base for 3 event module tests (18 shared tests)
  • Booking test base — New BookingModuleTestBase abstract test base for 6 booking module tests (16 shared tests)
  • WC handler cleanup — Removed unused $client_fn property from WC_Shipping_Handler and WC_Inventory_Handler

3.9.0

19 Feb 20:02

Choose a tag to compare

Fixed (Architecture)

  • safe_callback() preserves filter chainHook_Lifecycle::safe_callback() now returns the callback's result (was void). On crash, returns $args[0] so filter chains are not broken by a crashed callback
  • Module materialization failure UXModule_Registry::materialize() now records a version_warning entry when a module's constructor throws, making the failure visible in the admin health dashboard instead of being silently swallowed
  • Advisory Lock reentrancy guardAdvisory_Lock::acquire() returns true immediately if the lock is already held by the current instance, preventing redundant GET_LOCK() calls
  • Advisory Lock destructor safetyAdvisory_Lock::__destruct() releases the lock if still held (safety net for uncaught exceptions), wrapped in try-catch so destructors never throw
  • many2one_to_id rejects zero/negative IDsField_Mapper::many2one_to_id() now returns null for array-path IDs ≤ 0, preventing invalid Odoo references from propagating
  • Logger error_log fallbackLogger::flush_buffer() writes error/critical entries to PHP error_log() when $wpdb is unavailable (late shutdown), preventing silent log loss
  • cached_company_id multisite invalidationSync_Orchestrator::maybe_inject_company_id() now compares blog_id against the cached value, auto-invalidating the company_id cache when switch_to_blog() changes context
  • CLI try/finally on switch_to_blogCLI::sync(), reconcile(), and cleanup() now wrap their body in try/finally to guarantee restore_current_blog() is called even on exceptions
  • Microsecond date parsingField_Mapper::odoo_date_to_wp() now tries Y-m-d H:i:s.u format first, supporting Odoo timestamps with microsecond precision
  • Reconciliation batch size filterReconciler::reconcile() batch size is now filterable via wp4odoo_reconcile_batch_size, with ≤ 0 guard falling back to the default (200)
  • Translation cache flush — New Translation_Service::flush_caches() static method clears ir.translation and active languages transients. Called from CLI cache flush command
  • Filter return type validationmap_to_odoo() and pull_from_odoo() now validate that apply_filters() returns an array, falling back to the pre-filter value if a third-party filter returns an unexpected type
  • Stale recovery clears processed_atSync_Queue_Repository::recover_stale_processing() now resets processed_at to NULL when recovering jobs to 'pending', restoring the semantic contract (NULL = not yet processed) and preventing phantom timestamps in monitoring dashboards
  • Recovery transaction try-finallySync_Queue_Repository::recover_stale_processing() now wraps the SAVEPOINT/RELEASE block in try/finally, guaranteeing the transaction is committed even if a query throws
  • Translation buffer flush loss protectionTranslation_Accumulator::flush_pull_translations() now uses per-model try-catch. Failed models retain their buffer entries for the next flush instead of being silently discarded
  • Oversized payload observabilitySync_Queue_Repository::enqueue() now fires wp4odoo_enqueue_rejected action when a payload exceeds 1 MB, providing a hook for logging/monitoring instead of failing silently
  • WooCommerce safe_callback consistencywp4odoo_batch_processed hook in WooCommerce module now wrapped in safe_callback() for consistency with all other hook registrations
  • Compatibility report URL constructionAdmin::build_compat_report_url() now places query parameters before the #forms anchor. Previously parameters were appended after the fragment, making them invisible to the WPForms URL prefill parser
  • Compatibility report module field — Updated WPForms field ID from _1 (dropdown) to _14 (text input) for the module name, allowing any module name to be prefilled without dropdown restrictions

Fixed (Multisite)

  • Network-wide deactivation cron cleanupdeactivate() now iterates over all sites via get_sites() when network-deactivated, preventing orphaned cron hooks on subsites (mirrors existing activate() pattern)

Changed (Core)

  • Schema cache TTL reduced to 4 hoursSchema_Cache::CACHE_TTL changed from DAY_IN_SECONDS (24 h) to 4 * HOUR_IN_SECONDS. Odoo schema changes (new fields, renamed models) are now picked up within 4 hours instead of 24
  • Stats cache invalidation moved to batch level — Removed per-enqueue() call to invalidate_stats_cache() (was causing transient thrashing on high-throughput sites). Stats cache is now only invalidated after batch processing completes in Sync_Engine::run_with_lock(), where it was already called
  • Exclusive group admin warningModule_Registry::register() now records a version_warnings entry when a module is blocked by an exclusive group, identifying the active module and group name (e.g. "Sales was not started because woocommerce is already active in the ecommerce group")
  • safe_callback() crash counterHook_Lifecycle now tracks a static $crash_count incremented on each caught \Throwable. Exposed via get_crash_count() for the health dashboard, making silent graceful degradation events visible to administrators
  • Schema_Cache::flush() in test teardownModule_Test_Case::reset_static_caches() now calls Schema_Cache::flush() to prevent cross-test schema cache leakage
  • Partner_Service static reset in test teardown — New Partner_Helpers::reset_partner_service() method, called from Module_Test_Case::reset_static_caches() to prevent cross-test partner service leakage

Tests

  • Added Logger::flush_buffer() calls in MultisiteScopingTest integration tests to account for deferred buffer writes

3.8.0

19 Feb 05:50

Choose a tag to compare

Added

  • FluentBooking module — New booking module extending Booking_Module_Base. Syncs calendars → product.product (service), bookings → calendar.event (bidirectional). Hooks: fluent_booking/after_booking_scheduled, fluent_booking/booking_status_changed, fluent_booking/after_calendar_created, fluent_booking/after_calendar_updated. Tables: fluentbooking_calendars, fluentbooking_bookings. Version bounds: 1.0–1.5
  • Modern Events Calendar (MEC) module — New events module extending Events_Module_Base. Dual-model: probes Odoo for event.event, falls back to calendar.event. Syncs events (bidirectional) and MEC Pro bookings → event.registration (push-only). Hooks: save_post_mec-events, mec_booking_completed. CPT mec-events + custom mec_events table. Translatable fields (name, description). Version bounds: 6.0–7.15
  • FooEvents for WooCommerce module — New events module extending Events_Module_Base, required_modules: ['woocommerce']. Dual-model: event.event / calendar.event. Syncs WC event products (bidirectional) and ticket holders → event.registration (push-only). Hooks: save_post_product (priority 20), save_post_event_magic_tickets. Detects FooEvents products via WooCommerceEventsEvent meta. Version bounds: 1.18–2.0
  • Wholesale Suite pricelist sync (WC B2B extension) — Extends existing WC B2B module with pricelist entity type → product.pricelist. Pushes wholesale roles as Odoo pricelists, optionally assigns property_product_pricelist on wholesale partners. New hooks: wwp_wholesale_role_created, wwp_wholesale_role_updated. Role → pricelist mapping stored in wp_options. New settings: sync_pricelists, assign_pricelist_to_partner
  • Events exclusive group — The Events Calendar and MEC modules now share exclusive_group = 'events'. Only one events module boots per site (TEC has priority via registration order)
  • Events_Module_Base intermediate base class — Extracted shared logic from Events Calendar, MEC, and FooEvents into a new abstract base. Provides dual-model detection (event.event / calendar.event fallback), attendance resolution (partner + event mapping), shared event formatting/parsing, dedup domains, translation fields, and push/pull overrides. 5 abstract methods for subclass configuration. Events Calendar extends with ticket entity type overrides

Fixed (Architecture)

  • Multisite credential cache isolationOdoo_Auth::$credentials_cache is now keyed by blog_id. Previously a single static value, causing cross-site credential leakage when switch_to_blog() was called within the same request
  • flush_schema_cache nonce domain — Added flush_schema_cache to Admin_Ajax::ACTION_NONCE_MAP. Was falling back to the generic wp4odoo_admin nonce domain instead of the correct wp4odoo_setup
  • Webhook token encryption markerSettings_Repository now prefixes encrypted webhook tokens with enc1:. Eliminates a race condition during migration where a previously encrypted token could be double-encrypted. Three-path read logic: prefixed (current), legacy encrypted (no prefix but decryptable), plaintext (auto-migrates)
  • Batch dedup eviction trackingBatch_Create_Processor::group_eligible_jobs() now marks evicted duplicate job IDs in $batched_job_ids, preventing the individual processing loop from reprocessing jobs already replaced by newer duplicates for the same wp_id
  • save_wp_data failure now TransientSync_Orchestrator::pull_from_odoo() classifies save_wp_data() returning 0 as Error_Type::Transient (was Permanent). WordPress save failures (DB locks, temporary constraint issues) are retryable and should not permanently fail the job

Changed (Core)

  • Module settings cacheSettings_Repository::get_module_settings() now caches results in memory, keyed by blog_id + module ID. Avoids repeated get_option() calls when multiple hook callbacks read the same module settings in a single request. Cache invalidated on save_module_settings()
  • Circuit-breaker job deferralSync_Engine now defers jobs to scheduled_at + recovery_delay when a module's circuit breaker is open (was silently skipping). Prevents hot-polling: deferred jobs won't be re-fetched until the recovery window has passed
  • Logger write bufferLogger::log() now buffers entries in a static $write_buffer (threshold: 50). Flushes at threshold or via register_shutdown_function(). Reduces DB round-trips during batch processing where dozens of log calls occur per request
  • Shared Partner_ServicePartner_Helpers::$shared_partner_service is now a static singleton shared across all module instances. Avoids duplicate Odoo lookups when multiple modules resolve the same email in a single batch
  • Schema-guarded company_id injectionSync_Orchestrator::maybe_inject_company_id() now checks Schema_Cache::get_fields() before injecting company_id. Skips injection for models that don't have the field (e.g. product.template), avoiding Odoo validation errors

Tests

  • Added Logger::flush_buffer() calls in LoggerTest, WPAllImportModuleTest, and WebhookReliabilityTest to account for deferred buffer writes

3.7.0

19 Feb 02:19

Choose a tag to compare

Added

  • Forms module: Elementor Pro, Divi & Bricks support — Extends the existing Forms module with 3 new form plugin integrations. Hooks: elementor_pro/forms/new_record, et_pb_contact_form_submit, bricks/form/custom_action. Field extraction via Form_Field_Extractor normalizer. 3 new setting toggles
  • Food Ordering module: RestroPress support — Extends the existing Food Ordering module with RestroPress integration. Hooks: restropress_complete_purchase. Extraction via Food_Order_Extractor::extract_from_restropress(). New sync_restropress setting toggle
  • Fluent Support module — New helpdesk module extending Helpdesk_Module_Base. Syncs tickets → helpdesk.ticket (Enterprise) with project.task fallback (Community). Hooks: fluent_support/ticket_created, fluent_support/ticket_updated, fluent_support/response_added. Configurable Helpdesk Team ID and Project ID
  • Sensei LMS module — New LMS module extending LMS_Module_Base. Syncs courses → product.product (bidirectional), orders → account.move (push, auto-post), enrollments → sale.order (push, synthetic ID encoding). Hooks: save_post_course, sensei_course_status_updated, woocommerce_order_status_changed (filtered for Sensei orders). Version bounds: Sensei 4.0–4.24
  • Ultimate Member module — New user profile module. Syncs profiles → res.partner (bidirectional), roles → res.partner.category (push-only). Custom field enrichment (phone, company, country, city). Hooks: um_after_user_updated, um_registration_complete, um_delete_user, um_member_role_upgrade, um_member_role_downgrade. Version bounds: UM 2.6–2.9
  • WC Rental module — New WooCommerce extension module. Syncs rental orders → sale.order with Odoo Rental fields (is_rental, pickup_date, return_date). Generic approach: configurable meta keys for rental product detection and date extraction. Requires WooCommerce module. Hook: woocommerce_order_status_changed (priority 20, after WC module)
  • Field Service module — New bidirectional CPT module. Syncs field service tasks between wp4odoo_fs_task CPT and Odoo field_service.task (Enterprise). Admin-visible CPT under WP4Odoo menu. Status mapping (draft↔New, publish↔In Progress, private↔Done). Meta fields: planned date, deadline, priority. Dedup by task name
  • Odoo_Model enum: FieldServiceTask — New field_service.task case for Field Service module
  • WC_Order_Item::get_meta() — Added get_meta() method to WC_Order_Item stubs (PHPStan + PHPUnit) for order item meta access
  • Schema_Cache::flush_all() — New method that deletes both in-memory and persistent (transient) caches in one query. Fires wp4odoo_schema_cache_flushed action after cleanup for third-party extensibility
  • WP-CLI cache flush commandwp wp4odoo cache flush clears the Odoo schema cache (memory + transients) on demand
  • wp4odoo_retry_delay filter — Retry delay in Sync_Job_Tracking::handle_failure() is now filterable. Clamped to [0, MAX_RETRY_DELAY] for safety

Fixed (Architecture)

  • Exponential backoff capSync_Job_Tracking::calculate_retry_delay() now caps retry delay at 3600s (MAX_RETRY_DELAY). Without a cap, high attempt counts produced delays exceeding practical bounds
  • Stale recovery loop preventionSync_Queue_Repository::recover_stale_processing() now increments attempts when recovering stale jobs. Prevents infinite recovery loops where a job that consistently crashes is recovered and reprocessed indefinitely without progressing toward max_attempts (reverses 3.6.0 "no-increment" behavior — a job that repeatedly stalls IS progressing toward failure)
  • SQL injection hardeningSync_Queue_Repository::enqueue() and cleanup() now use $wpdb->prepare() for status IN (...) clauses (was safe hardcoded values, but inconsistent with the fully-prepared pattern used elsewhere)
  • Atomic stale recovery guardSync_Engine::process_queue() now uses wp_cache_add() for the stale recovery mutex (was non-atomic get_transient()/set_transient() — two concurrent crons could both pass the check)
  • Multisite settings cache scopingSettings_Repository internal cache is now keyed by blog_id. On multisite, switch_to_blog() previously returned cached settings from the originating site

Fixed (Multisite)

  • Credential cache flush on switch_blogwp4odoo.php now hooks switch_blog to call Odoo_Auth::flush_credentials_cache(), preventing stale credentials from site A leaking into site B when using switch_to_blog()

Fixed (Modules)

  • Sales_Module handler init — Moved Portal_Manager and Partner_Service initialization from boot() to __construct(), consistent with all other modules. Prevents potential uninitialized property access if portal_manager is referenced before boot() is called

Changed

  • SSL verification warningOdoo_JsonRPC and Odoo_XmlRPC now log a warning when WP4ODOO_DISABLE_SSL_VERIFY is active, making the insecure configuration visible in logs
  • PHP 8.1+ first-class callable syntax — Replaced 17 string-based callables ('intval', 'strval') with first-class callable syntax (intval( ... ), strval( ... )) across 9 files. Better IDE support, type-safe, catches typos at parse time
  • PHP 8.0+ str_contains() — Replaced 2 strpos() !== false patterns with str_contains() in GamiPress_Handler

CI

  • Composer audit — Added composer audit --no-dev step to GitHub Actions CI pipeline to detect known vulnerabilities in dependencies

Tests

  • Updated SyncQueueRepositoryTest — 2 tests adapted: stale recovery now asserts attempts increment, cleanup test matches prepared IN (%s, %s) pattern
  • Removed duplicate documents-classes.php stub require in tests/bootstrap.php

3.6.0

18 Feb 23:24

Choose a tag to compare

Added

  • WP ERP Accounting module — Completes the WP ERP triptyque (HR + CRM + Accounting). Syncs journal entries → account.move (bidirectional), chart of accounts → account.account (bidirectional), journals → account.journal (bidirectional). Custom table access (erp_acct_journals, erp_acct_ledger_details, erp_acct_chart_of_accounts). Invoice status mapping (draft/awaiting_payment/paid/overdue/void → Odoo states). Hooks: erp_acct_new_journal, erp_acct_new_invoice, erp_acct_update_invoice, erp_acct_new_bill, erp_acct_new_expense
  • LearnPress module — LMS module for LearnPress 4.0+ (100k+ installations). Extends LMS_Module_Base. Syncs courses → product.product (bidirectional), orders → account.move (push, auto-post), enrollments → sale.order (push, synthetic ID encoding). Translation support for courses. Hooks: save_post_lp_course, learn-press/order/status-completed, learn-press/user/course-enrolled
  • Food Ordering module — Aggregate module for GloriaFood + WPPizza → Odoo POS Restaurant. Push-only, syncs food orders → pos.order with pos.order.line One2many tuples. Strategy-based extraction via Food_Order_Extractor. Per-plugin detection and setting toggles. Hooks: save_post_flavor_order, wppizza_order_complete
  • Survey & Quiz module — Aggregate module for Quiz Maker (Ays) + Quiz And Survey Master → Odoo Survey. Push-only, syncs quizzes → survey.survey with question_and_page_ids One2many, responses → survey.user_input with user_input_line_ids One2many. Strategy-based extraction via Survey_Extractor. Question type mapping (radio→simple_choice, checkbox→multiple_choice, etc.)
  • myCRED module — Points & rewards module for myCRED 2.0+ (10k+ installations). Syncs point balances → loyalty.card (bidirectional via Loyalty_Card_Resolver), badge types → product.template (push-only, service products). Anti-loop via odoo_sync reference guard. Configurable Odoo loyalty program ID. Hooks: mycred_update_user_balance, mycred_after_badge_assign
  • Jeero Configurator module — Product configurator module for Jeero WC Product Configurator. Push-only, syncs configurable products → mrp.bom with bom_line_ids One2many tuples. Cross-module entity_map resolution for WooCommerce product → Odoo product.template ID. Transient error pattern for unsynced component dependencies. Requires WooCommerce module. Hooks: save_post_product (filtered for configurable products)
  • Documents module — Bidirectional document sharing between WordPress and Odoo Documents (Enterprise). Supports WP Document Revisions (document CPT) and WP Download Manager (wpdmpro CPT). Syncs documents → documents.document (bidirectional, base64 file encoding, SHA-256 change detection), folders → documents.folder (bidirectional, hierarchy via parent_folder_id). Hooks: save_post_document, save_post_wpdmpro, before_delete_post, created_document_category, edited_document_category
  • Odoo_Model enum additions — 8 new cases: AccountJournal, AccountAccount, PosOrder, PosOrderLine, SurveySurvey, SurveyUserInput, DocumentsDocument, DocumentsFolder
  • WC Inventory module — Advanced multi-warehouse stock management with optional ATUM Multi-Inventory integration. Syncs warehouses (stock.warehouse, pull-only), stock locations (stock.location, pull-only), and stock movements (stock.move, bidirectional). Complements the WooCommerce module's global stock.quant sync with individual move tracking. Hooks at priority 20 on woocommerce_product_set_stock (after WC module at 10). ATUM detection at runtime for multi-location support
  • WC Shipping module — Bidirectional shipment tracking sync with optional ShipStation, Sendcloud, Packlink, and AST integration. Pushes WC tracking data to Odoo stock.picking (carrier_tracking_ref), pulls Odoo shipment tracking back to WC order meta (AST-compatible format). Optionally syncs WC shipping methods as delivery.carrier records. Provider-specific extraction methods for each shipping plugin
  • WC Returns module — Full return/refund lifecycle with optional YITH WooCommerce Return & Warranty and ReturnGO integration. Pushes WC refunds as Odoo credit notes (account.move with move_type=out_refund), pulls credit notes back as WC refunds. Optionally creates return stock.picking entries. Auto-posts credit notes via Odoo_Accounting_Formatter::auto_post(). Cross-module entity resolution for original invoice (reversed_entry_id)
  • Odoo_Accounting_Formatter::for_credit_note() — New static method for formatting credit note data (out_refund account.move). Reuses build_invoice_lines() for line items, supports optional reversed_entry_id for linking to original invoice. Filterable via wp4odoo_credit_note_data hook
  • Odoo_Model enum additions — 5 new cases: StockWarehouse, StockLocation, StockMove, StockQuant, StockPickingType

Added (Architecture)

  • GamiPress/myCRED gamification exclusive group — GamiPress and myCRED both target loyalty.card via Loyalty_Card_Resolver with (partner_id, program_id). New gamification exclusive group prevents both from running simultaneously (first-registered wins: GamiPress before myCRED)
  • Lazy loading Module_Registry — Disabled modules are no longer instantiated at boot. register_all() stores disabled module class names in a $deferred map; get() materializes on demand, all() materializes everything (for admin UI). Reduces memory footprint on sites with many detected but disabled modules
  • Module health manifest — New tests/module-health-manifest.php declares all third-party symbols (classes, functions, constants) per module. ModuleHealthManifestTest validates that stubs match the manifest and that the manifest covers all registered modules. 196 data-driven tests catch stub drift when upstream plugins evolve
  • Sync flow integration tests — 5 new integration tests (tests/Integration/SyncFlowTest.php) covering the full push/pull pipeline: hook → queue → process → Odoo transport → entity_map. Uses SyncFlowTransport mock for deterministic transport responses
  • Per-module circuit breaker — New Module_Circuit_Breaker class isolates failing modules without blocking the entire sync. Dual-level design: existing global Circuit_Breaker handles transport failures (Odoo down), new module breaker handles per-module failures (model uninstalled, access rights). Threshold: 5 consecutive batches with ≥80% failure ratio. Recovery delay: 600s (half-open probe). State stored in wp4odoo_module_cb_states option. Integrated into Sync_Engine (per-module outcome tracking), Failure_Notifier (per-module email with cooldown), and health dashboard (open modules display). Auto-cleans stale state older than 2 hours
  • Entity_Map orphan cleanup — New Entity_Map_Repository::cleanup_orphans() method detects and removes entity_map entries where the WP post no longer exists (LEFT JOIN against wp_posts). Excludes user-based modules (BuddyBoss, FluentCRM, etc.). New WP-CLI command: wp wp4odoo cleanup orphans [--module=<module>] [--dry-run] [--yes]
  • Trait extraction refactoring — 6 new traits extracted from 3 large classes to improve separation of concerns:
    • Hook_Lifecycle (from Module_Base): $registered_hooks, safe_callback(), register_hook(), teardown()
    • Translation_Accumulator (from Module_Base): $translation_buffer, get_translatable_fields(), accumulate_pull_translation(), flush_pull_translations()
    • Sync_Job_Tracking (from Sync_Engine): $batch_failures, $batch_successes, $module_outcomes, handle_failure(), record_module_outcome()
    • Failure_Tracking_Settings (from Settings_Repository): consecutive failure count and last failure email timestamp
    • UI_State_Settings (from Settings_Repository): onboarding/checklist dismissed flags, webhook confirmation, cron health
    • Network_Settings (from Settings_Repository): multisite network connection, site → company_id mapping

Fixed (Architecture)

  • LRU cache eviction ratioEntity_Map_Repository::evict_cache() now keeps 75% of entries during eviction (was 50%). Prevents counter-productive cache thrashing during batch operations where frequent eviction caused redundant DB queries
  • Stale job recoverySync_Queue_Repository::recover_stale_processing() no longer increments attempts when recovering interrupted jobs. A stale-processing job was interrupted (crash/timeout), not genuinely failed — incrementing attempts caused premature failure at max_attempts
  • Circuit breaker TTLCircuit_Breaker transient and DB state TTL now uses RECOVERY_DELAY × 2 (600s) instead of HOUR_IN_SECONDS (3600s). During long Odoo outages (>1h), the old state expired and the circuit re-closed, sending a burst of requests to a still-down server. Stale DB state discard increased from 1h to 2h
  • Transactional migrationsDatabase_Migration::run_migrations() now wraps each migration in a START TRANSACTION / COMMIT block with ROLLBACK on failure, preventing partial schema changes from leaving tables in an inconsistent state
  • Dual accounting entity type validationDual_Accounting_Module_Base::load_wp_data() now validates $entity_type against the known parent/child types before processing. Invalid entity types are logged and return empty data instead of silently falling through
  • Batch create intra-group dedupBatch_Create_Processor::group_eligible_jobs() now deduplicates by wp_id within each module:entity_type group. Prevents creating duplicate Odoo records when two create jobs for the same WP entity are in the same batch
  • JSON decode error classificationBatch_Create_Processor now classifies invalid JSON payloads as Error_Type::Permanent (was Transient). A corrupted payload will never self-fix, so retrying wastes 2 attem...
Read more

3.5.0

18 Feb 03:06

Choose a tag to compare

Added

  • WordPress Multisite → Multi-company Odoo — Full multisite support: each site in a WordPress network syncs with a specific Odoo company (res.company). Migration 7 adds blog_id column to wp4odoo_entity_map, wp4odoo_sync_queue, and wp4odoo_logs tables. Repository layer (Entity_Map_Repository, Sync_Queue_Repository, Logger) scopes all queries by blog_id. Odoo_Client injects allowed_company_ids into kwargs context when company_id > 0. Network activation iterates all sites, wp_initialize_site hook provisions new sites. Backward compatible: DEFAULT 1 ensures single-site installs are unaffected
  • Network Admin page — Centralized settings page in the WordPress network admin for shared Odoo connection (URL, DB, user, API key, protocol, timeout) and site → company_id mapping. Individual sites inherit the network connection by default but can override with their own connection in Settings > Odoo Connector
  • JetBooking module — New booking module for JetBooking (Crocoblock) 3.0+. Extends Booking_Module_Base. Syncs services → product.product (bidirectional), bookings → calendar.event (push). Hybrid data access: CPT for services, custom table jet_apartment_bookings for bookings. Hooks: jet-abaf/db/booking/after-insert, jet-abaf/db/booking/after-update, save_post_{service_cpt}
  • WP ERP CRM module — New CRM module for WP ERP 1.6+. Syncs contacts → crm.lead (bidirectional), activities → mail.activity (push). Custom table access (erp_peoples, erp_crm_customer_activities). Activity type resolution via mail.activity.type (cached). Cross-module partner linking with existing CRM module via email lookup. Life stage mapping: subscriber/lead → lead, opportunity → opportunity, customer → won
  • JetEngine Meta-Module — New meta-module for JetEngine (Crocoblock) 3.0+. Enriches other modules' sync pipelines with JetEngine meta-fields (pattern identical to ACF meta-module). Admin-configured mappings: target_module, entity_type, jet_field, odoo_field, type. Registers wp4odoo_map_to_odoo_*, wp4odoo_map_from_odoo_*, and wp4odoo_after_save_* filters at boot. No own entity types
  • JetFormBuilder support — 8th form plugin in the Forms module. Hooks into jet-form-builder/form-handler/after-send. Key-value data extraction via strategy pattern (same as Fluent Forms). Detection: JET_FORM_BUILDER_VERSION constant
  • Odoo_Model enum additions — 2 new cases: MailActivity, MailActivityType
  • WP-CLI --blog_id parameterwp wp4odoo sync run --blog_id=3 and wp wp4odoo reconcile crm contact --blog_id=3 target a specific site in multisite (switches blog context, flushes credential cache)

Added (Architecture)

  • Migration 8 — Stale recovery index — New composite index idx_stale_recovery (blog_id, status, processed_at) on wp4odoo_sync_queue for efficient stale job recovery queries. Idempotent (checks SHOW INDEX before ALTER)
  • Migration 9 — Rebuild indexes with blog_id prefix — Rebuilds idx_dedup_odoo on wp4odoo_sync_queue and idx_poll_detection on wp4odoo_entity_map with blog_id as leading column for correct multisite index scoping. Idempotent (checks SHOW INDEX before DROP)
  • Migration 10 — Logs cleanup index + drop obsolete index — Adds idx_blog_cleanup (blog_id, created_at) on wp4odoo_logs for efficient multisite log cleanup. Drops obsolete idx_dedup_composite from wp4odoo_sync_queue (redundant since migration 7 added blog_id to dedup indexes)
  • Logger::for_channel() factory — New static factory method creates Logger instances with a shared Settings_Repository (avoids repeated get_option() calls when multiple loggers are created in the same request). All 12 direct new Logger() callsites migrated to Logger::for_channel() (Sync_Engine, Queue_Manager, Module_Base, Webhook_Handler, Partner_Service, Odoo_Client, Odoo_Transport_Base, Odoo_Auth, CLI, Ajax_Monitor_Handlers, Translation_Service)
  • Module_Base::register_hook() / teardown() — New hook lifecycle methods. register_hook() wraps add_action() with safe_callback() and tracks registered hooks. teardown() removes all tracked hooks via remove_action(). Called automatically when a module is disabled via the admin toggle. Designed for gradual adoption by new modules
  • Queue_Manager injectable Logger — Constructor accepts optional ?Logger $logger parameter. When provided (e.g. by Sync_Engine), queue depth alerts share the caller's Logger and its correlation ID. Falls back to Logger::for_channel('queue_manager') when null

Removed

  • exclusive_priority dead code — Removed $exclusive_priority property and get_exclusive_priority() getter from Module_Base and 17 module classes. Module_Registry::register() never read this value — exclusive groups use first-registered-wins based on registration order, not priority

Fixed (Architecture)

  • Blog-scoped transientsSync_Engine stale recovery transient (wp4odoo_last_stale_recovery) and Queue_Manager depth check cooldown now include get_current_blog_id() in their transient keys, preventing multisite sites from sharing cooldown state
  • Webhook token auto-migrationSettings_Repository::get_webhook_token() now auto-encrypts plaintext tokens on first read (backward-compat fallback path). Subsequent reads return the encrypted-then-decrypted value, eliminating the plaintext storage after first access
  • Multisite index scopingidx_dedup_odoo and idx_poll_detection indexes were missing blog_id as leading column, causing full-index scans in multisite installations. Migration 9 rebuilds both with correct blog_id prefix
  • SSRF protection on Network Admin — Network_Admin URL input now passes through Settings_Validator::is_safe_url() (same SSRF check as Settings_Page), rejecting private/internal IP addresses. is_safe_url() and is_private_ip() extracted from Settings_Page to shared Settings_Validator
  • Encryption key empty saltOdoo_Auth::get_encryption_key() now throws RuntimeException when both AUTH_KEY and SECURE_AUTH_KEY are empty, instead of silently deriving a predictable key from SHA256('')
  • Batch JSON error classificationBatch_Create_Processor now classifies invalid JSON payloads as Error_Type::Transient (retryable) instead of Permanent, consistent with Sync_Engine::process_job() behavior
  • AJAX exception message sanitizationAjax_Data_Handlers (fetch_odoo_taxes, fetch_odoo_carriers) no longer exposes raw exception messages to the browser. Errors are logged via Logger::for_channel('admin') and a generic translated message is returned to the client

Changed

  • Settings_Repository multisite support — New methods: get_effective_connection() (local → network fallback), get_network_connection(), save_network_connection(), get_site_company_id(), get_network_site_companies(), save_network_site_companies(), is_using_network_connection(). New constants: OPT_NETWORK_CONNECTION, OPT_NETWORK_SITE_COMPANIES. company_id added to DEFAULTS_CONNECTION
  • Odoo_Auth multisite credential resolutionget_credentials() falls back to network-level shared connection when site has no local URL configured. Applies site-specific company_id from network mapping. save_credentials() preserves company_id
  • Sync_Engine lock scoping — Advisory lock names include blog_id: wp4odoo_sync_{blog_id} and wp4odoo_sync_{blog_id}_{module} for multisite isolation
  • Connection tab UI — Shows "Using network connection" indicator when site inherits from network. New Company ID input field after Timeout

Tests

  • 4 188 unit tests (6 445 assertions) — new tests covering multisite blog_id scoping (Entity_Map_Repository, Sync_Queue_Repository), company_id injection (Odoo_Client), credential resolution (Odoo_Auth network fallback), Settings_Repository multisite methods, switch_to_blog stubs, JetBooking module+handler, WP ERP CRM module+handler, JetEngine Meta-Module, JetFormBuilder form extraction, migrations 7–10, webhook token auto-migration

3.4.0

17 Feb 23:14

Choose a tag to compare

Added

  • Dokan module — New marketplace module for Dokan 3.7+. Syncs vendors → res.partner (bidirectional, supplier_rank=1), sub-orders → purchase.order (push), commissions → account.move (push, vendor bills via Odoo_Accounting_Formatter), withdrawals → account.payment (push). Exclusive group marketplace. Requires WooCommerce module
  • WCFM module — New marketplace module for WCFM Marketplace 6.5+. Syncs vendors → res.partner (bidirectional, supplier_rank=1), sub-orders → purchase.order (push), commissions → account.move (push, vendor bills), withdrawals → account.payment (push). Exclusive group marketplace. Requires WooCommerce module
  • WC Vendors module — New marketplace module for WC Vendors Pro 2.0+. Syncs vendors → res.partner (bidirectional, supplier_rank=1), sub-orders → purchase.order (push), commissions → account.move (push, vendor bills), payouts → account.payment (push). Exclusive group marketplace. Requires WooCommerce module
  • SureCart module — New e-commerce module for SureCart 2.0+. Syncs products → product.template (bidirectional), orders → sale.order (bidirectional), subscriptions → sale.subscription (push). Exclusive group ecommerce_alt
  • WC B2B module — New B2B/wholesale module for Wholesale Suite (WWP) 2.0+. Syncs company accounts → res.partner (bidirectional, is_company=true, payment terms, partner categories), pricelist rules → product.pricelist.item (push, wholesale pricing). Requires WooCommerce module
  • MailPoet module — New email marketing module for MailPoet 4.0+. Syncs subscribers → mailing.contact (bidirectional, M2M list_ids resolution), mailing lists → mailing.list (bidirectional). Dedup by email/name
  • Mailchimp for WP (MC4WP) module — New email marketing module for MC4WP 4.8+. Syncs subscribers → mailing.contact (bidirectional, M2M list_ids resolution), lists → mailing.list (bidirectional). Hooks into mc4wp_form_subscribed and mc4wp_integration_subscribed. Dedup by email/name
  • JetAppointments module — New booking module for JetAppointments (Crocoblock) 1.0+. Extends Booking_Module_Base. Syncs services → product.product (bidirectional), appointments → calendar.event (push). CPT-based data access (jet-appointment, jet-service). Hooks: jet-apb/db/appointment/after-insert, jet-apb/db/appointment/after-update, save_post_{service_cpt}
  • WP Project Manager module — New project management module for WP Project Manager (weDevs) 2.0+. Syncs projects → project.project (bidirectional), tasks → project.task (bidirectional), timesheets → account.analytic.line (push). Custom table access (cpm_projects, cpm_tasks). Employee resolution via hr.employee lookup. Dependency cascade: tasks require project synced first
  • WC Product Add-Ons module — New product add-ons module supporting WooCommerce Product Add-Ons (official 6.0+), ThemeHigh THWEPO, and PPOM. Push-only with dual mode: product_attributes (add-ons → product.template.attribute.line) or bom_components (add-ons → mrp.bom). Multi-plugin abstraction layer auto-detects active plugin. Cross-module entity_map for parent product resolution
  • JetEngine module — New generic CPT sync module for JetEngine (Crocoblock) 3.0+. Push-only with admin-configured CPT → Odoo model mappings. Dynamic entity types populated from cpt_mappings settings at runtime. Unified field reader: standard post fields, meta:, jet:, tax: prefixes. 8 type conversions. Per-mapping dedup field. PHP filter hooks for customization
  • Odoo_Model enum additions — 11 new cases: MailingContact, MailingList, PurchaseOrder, AccountPaymentTerm, PartnerCategory, ProductCategory, ProjectProject, AccountAnalyticLine, ProductAttribute, ProductAttributeValue, ProductTemplateAttributeLine
  • Queue depth alertingQueue_Manager monitors pending job count and fires wp4odoo_queue_depth_warning (≥ 1 000 jobs) and wp4odoo_queue_depth_critical (≥ 5 000 jobs) action hooks with a 5-minute cooldown between alerts. Consumers can use these to trigger admin notices, email alerts, or pause enqueuing
  • Queue_Job readonly DTO — New Queue_Job readonly class provides typed, immutable access to sync queue job data (id, module, entity_type, action, wp_id, odoo_id, status, retry_count, error_message, created_at, scheduled_at, claimed_at). Sync_Queue_Repository::fetch_pending() now returns Queue_Job[] instead of raw stdClass arrays
  • Advisory_Lock utility class — Reusable MySQL advisory lock wrapper (acquire(), release(), is_held()) consolidating 3 duplicate implementations across Sync_Engine, Partner_Service, and Push_Lock

Changed

  • Queue_Manager injectableQueue_Manager is now instantiable with an optional Sync_Queue_Repository dependency. Module_Base exposes queue() accessor and set_queue_manager() setter. Module push/pull operations use the injectable instance. Static API preserved for backward compatibility
  • Chunked DELETE in cleanupSync_Queue_Repository::cleanup() and Logger::cleanup() now delete in chunks of 10 000 rows to avoid long-running table locks on large sites
  • CLI i18n — 42+ WP_CLI::log() / WP_CLI::success() / WP_CLI::error() strings across CLI, CLI_Queue_Commands, and CLI_Module_Commands wrapped in __() for translation support
  • Injectable transport for Odoo_ClientOdoo_Client constructor now accepts optional ?Transport and ?Settings_Repository parameters, enabling test doubles without monkey-patching and improving DI testability
  • Standardized Logger constructionLogger now consistently receives Settings_Repository across all construction sites (Odoo_Client, Translation_Service, Ajax_Monitor_Handlers), eliminating bare new Logger() calls
  • Webhook_Handler dependency injectionWebhook_Handler now receives Module_Registry via constructor instead of resolving the plugin singleton internally
  • Advisory lock on batch createspush_batch_creates() acquires a per-model advisory lock (wp4odoo_batch_{module}_{model}) to prevent concurrent batch creates from producing duplicates
  • Settings write-time validationSettings_Repository gains save_sync_settings() and save_log_settings() with enum validation (protocol, direction, log level) and numeric range clamping (timeout 5–120, batch_size 1–500)
  • Rate_Limiter atomic increment — Dual-strategy rate limiting: wp_cache_incr() for sites with Redis/Memcached (atomic, no TOCTOU), transient fallback for standard installs
  • Module_Helpers trait split — Split Module_Helpers into 4 focused sub-traits: Partner_Helpers, Accounting_Helpers, Dependency_Helpers, Sync_Helpers. Module_Helpers now composes all 4 via use for backward compatibility

Fixed

  • Exclusive group priority bugModule_Registry::has_booted_in_group() now blocks any module in the same exclusive group regardless of priority (first-registered wins), fixing a bug where higher-priority modules could bypass exclusion

Refactored

  • Settings_Repository helpers — Extracted get_bool_option() / set_bool_option() / get_int_option() / set_int_option() private helpers, reducing 8 getter/setter pairs to one-liner delegations
  • Form_Field_Extractor — Extracted 7 extract_from_* methods from Form_Handler (514 → 97 LOC) into a strategy-based Form_Field_Extractor class with registered closures per form plugin
  • WC_Translation_Accumulator — Extracted translation accumulation logic from WC_Pull_Coordinator (723 → 520 LOC) into a focused WC_Translation_Accumulator class (289 LOC)
  • Translation strategies — Extracted Odoo version-specific push/pull logic from Translation_Service (768 → 629 LOC) into Translation_Strategy_Modern (Odoo 16+) and Translation_Strategy_Legacy (Odoo 14–15) behind a Translation_Strategy interface
  • Sync_Orchestrator trait — Extracted push_to_odoo(), push_batch_creates(), and pull_from_odoo() from Module_Base (1 245 → 921 LOC) into a Sync_Orchestrator trait (344 LOC)
  • Batch_Create_Processor — Extracted batch create pipeline from Sync_Engine (811 → 731 LOC) into a dedicated Batch_Create_Processor class (208 LOC) with injected dependencies
  • Test base classes — Extracted MembershipModuleTestBase (30 shared tests) and LMSModuleTestBase (29 shared tests) abstract classes, reducing 6 module test files by ~580 lines total

Tests

  • 4 051 unit tests (6 210 assertions) — new tests covering 11 new modules (Dokan, WCFM, WC Vendors, SureCart, WC B2B, MailPoet, MC4WP, JetAppointments, WP Project Manager, WC Product Add-Ons, JetEngine), Queue_Job DTO, advisory locks, Queue_Manager DI, queue depth alerting, chunked cleanup, settings validation, rate limiter, and module registry fixes

3.3.0

16 Feb 12:39

Choose a tag to compare

Added

  • Queue observability hook — New wp4odoo_job_processed action fires after each sync job with module ID, elapsed time (ms), Sync_Result, and raw job object. Enables external monitoring and performance dashboards
  • Retry visibility — Queue admin tab now shows a "Scheduled" column with human-readable countdown (e.g. "2 hours") for pending jobs with future scheduled_at, or "Now" for immediately-ready jobs
  • Module dependency graph — New Module_Base::get_required_modules() method declares inter-module dependencies. WC Subscriptions, WC Bookings, WC Bundle BOM, WC Points & Rewards, and WC Memberships now require the WooCommerce module to be active. Module_Registry enforces this at boot time and generates an admin warning if a dependency is missing

Changed

  • Error classification in module catch blocks — WC Points & Rewards, GamiPress, WC Pull Coordinator, and Stock Handler now use Error_Classification::classify_exception() instead of hardcoded Error_Type::Transient in catch blocks. Permanent errors (validation, access denied) are no longer retried unnecessarily
  • Loyalty_Card_Resolver trait extraction — Extracted shared find-or-create loyalty.card logic from WC Points & Rewards and GamiPress into a reusable Loyalty_Card_Resolver trait (~75 LOC deduplication)
  • CLI trait extraction — Extracted queue subcommands (stats, list, retry, cleanup, cancel) and module subcommands (list, enable, disable) from CLI class into CLI_Queue_Commands and CLI_Module_Commands traits for testability and separation of concerns
  • Rate_Limiter extraction — Extracted transient-based rate limiting from Webhook_Handler into a standalone Rate_Limiter class, parameterized by prefix, max requests, and window duration for reusability
  • push_entity() standardization — Converted 16 Queue_Manager::push() callsites across 8 hook traits (FluentCRM, FunnelKit, SimplePay, BuddyBoss, Amelia, RCP, WooCommerce, Membership, TutorLMS) to use the push_entity() helper, ensuring consistent should_sync() guards and automatic create/update determination via mapping lookup

Fixed

  • uninstall.php — Added missing wp4odoo_log_cleanup cron event cleanup (previously only wp4odoo_scheduled_sync was cleared)

3.2.5

15 Feb 23:53

Choose a tag to compare

Added

  • FunnelKit module — New funnel/sales pipeline module for FunnelKit (ex-WooFunnels) 3.0+. Syncs contacts → crm.lead (bidirectional) with stage progression, funnel steps → crm.stage (push-only). Configurable Odoo pipeline ID, filterable stage mapping via wp4odoo_funnelkit_stage_map
  • GamiPress module — New gamification/loyalty module for GamiPress 2.6+. Syncs point balances → loyalty.card (bidirectional, find-or-create by partner+program), achievement types → product.template (push-only), rank types → product.template (push-only). Same loyalty.card pattern as WC Points & Rewards
  • BuddyBoss module — New community module for BuddyBoss/BuddyPress 2.4+. Syncs profiles → res.partner (bidirectional, enriched with xprofile fields), groups → res.partner.category (push-only). Group membership reflected as partner category tags via Many2many [(6, 0, [ids])] tuples
  • WP ERP module — New HR module for WP ERP 1.6+. Syncs employees → hr.employee (bidirectional), departments → hr.department (bidirectional), leave requests → hr.leave (bidirectional). Dependency chain: leaves require employee synced first, employees require department synced first. Leave status mapping: pending/approved/rejected ↔ draft/validate/refuse (filterable via wp4odoo_wperp_leave_status_map)
  • Knowledge module — New content sync module for WordPress posts ↔ Odoo Knowledge articles (knowledge.article, Enterprise v16+). Bidirectional sync with HTML body preserved, parent hierarchy via entity map, configurable post type, optional category slug filter. Odoo-side availability guarded via model probe. WPML/Polylang translation support for name + body fields
  • Multi-batch queue processingSync_Engine now processes multiple batches per cron invocation (up to 20 iterations) until the time limit (55 s) or memory threshold (80%) is reached. Drains large queues 10–20× faster than single-batch
  • Push dedup advisory lockpush_to_odoo() now acquires a MySQL advisory lock before the search-before-create path, preventing TOCTOU race conditions where concurrent workers could create duplicate Odoo records for the same entity
  • Optimized cron pollingpoll_entity_changes() now uses targeted entity_map loading (IN (wp_ids)) instead of loading all rows, and detects deletions via last_polled_at timestamps instead of in-memory set comparison. New DB migration adds last_polled_at column to entity_map

Changed

  • Sync_Engine method extraction — Extracted should_continue_batching() and process_fetched_batch() from run_with_lock() for readability and testability
  • Module_Base trait extraction — Extracted 3 focused traits from Module_Base: Error_Classification (exception → Error_Type classification), Push_Lock (advisory lock for push dedup), Poll_Support (targeted poll with last_polled_at). Module_Base uses all 3 via use statements
  • CRM_Module error classificationpush_to_odoo() override now catches \RuntimeException and classifies errors via Error_Classification::classify_exception() instead of treating all failures as transient
  • Contact_Refiner resilience — All refinement methods (refine_name, refine_country, refine_state) now catch \Throwable and return unmodified data on failure, preventing a single refinement error from blocking the entire sync
  • Batch creates N+1 fixpush_batch_creates() now passes the pre-resolved $module instance to process_single_job() instead of re-resolving from Module_Registry for each job in the batch
  • Field_Mapper date format cacheconvert_date() now caches the Odoo date format string across calls, avoiding redundant format resolution per field

Tests

  • 33 new unit tests — 3 new test files:
    • ErrorClassificationTest (19 tests) — Error_Classification trait: HTTP 5xx → transient, access denied → permanent, network errors → transient, default → transient
    • PushDedupLockTest (6 tests) — Push_Lock trait: advisory lock acquire/release, lock on create path, no lock on update path, lock timeout → transient error
    • DatabaseMigrationTest (8 tests) — Migration 6: last_polled_at column addition, index creation, idempotency

3.2.0

15 Feb 20:36

Choose a tag to compare

Fixed

  • WC Bookings silent push failure — Entity type 'product' was not declared in WC_Bookings_Module's $odoo_models (only 'service' and 'booking'), causing every booking product push to silently fail. Changed to 'service'
  • Exclusive group mismatch — WooCommerce, Sales, and EDD modules used 'commerce' as their exclusive group while ARCHITECTURE.md documented 'ecommerce'. Unified to 'ecommerce'
  • Batch creates double failureSync_Engine::process_batch_creates() added jobs to $claimed_jobs before JSON validation, causing handle_failure() to be called twice (once for invalid JSON, once for batch error). Moved append after validation
  • SSRF bypass via DNS failureis_safe_url() returned true when gethostbyname() failed (returns the input on DNS failure), allowing URLs with unresolvable hostnames to bypass SSRF protection. Now returns false
  • Queue health metrics cache leakinvalidate_stats_cache() cleared wp4odoo_queue_stats but not wp4odoo_queue_health, leaving stale health metrics
  • Stale recovery orderingrecover_stale_processing() ran after fetch_pending(), so freshly recovered jobs were excluded from the current batch. Reordered to recover first
  • Odoo_Client retry missing action — Retry path after session re-auth did not fire wp4odoo_api_call action, making retry calls invisible to monitors
  • MySQL 5.7 compat@@in_transaction session variable query could produce a visible error on MySQL 5.7 (which lacks this variable). Wrapped with suppress_errors()
  • Dual accounting deleteresolve_accounting_model() was skipped for delete actions, causing delete calls to target the wrong Odoo model when OCA donation.donation was active
  • Helpdesk exclusive group/priorityHelpdesk_Module_Base used method overrides instead of properties for $exclusive_group and $exclusive_priority, inconsistent with all other intermediate bases. Converted to properties
  • Undefined $jobs variableSync_Engine::process_queue() could reference undefined $jobs if the try block threw before assignment
  • Partner email normalizationPartner_Service::get_or_create_batch() now trims and lowercases emails before Odoo lookup, preventing duplicate partners from case mismatches
  • Reconciler client hoistingReconciler resolved the Odoo client inside the per-entity loop instead of once before it
  • Logger context truncationtruncate_context() JSON-encoded the full array then truncated the string, producing invalid JSON. Now truncates the array first, then encodes
  • Empty encryption key warningOdoo_Auth now logs a warning via error_log() when the encryption key is empty, aiding diagnosis of misconfigured installations
  • CLI --format validationqueue stats and queue list subcommands now reject unsupported --format values with a clear error
  • Ecwid cron orphan on deactivationwp4odoo_ecwid_poll cron event was cleared on uninstall but not on plugin deactivation, leaving an orphaned cron entry. Added to deactivate()
  • Exclusive group priority documentation — ARCHITECTURE.md listed membership, invoicing, and helpdesk exclusive group priorities in reverse order (lower number shown as winning). Corrected to reflect actual >= logic where highest number wins

Added

  • Bidirectional WC stock sync — Stock push (WC → Odoo) via new Stock_Handler class. Version-adaptive API: stock.quant + action_apply_inventory() for Odoo 16+, stock.change.product.qty wizard for v14-15. Hooks: woocommerce_product_set_stock, woocommerce_variation_set_stock. Anti-loop guard prevents re-enqueue during pull
  • TutorLMS module — New LMS sync module for TutorLMS 2.6+. Syncs courses → product.product, orders → account.move, enrollments → sale.order. Bidirectional course sync, synthetic enrollment IDs, auto-post invoices. Extends LMS_Module_Base
  • FluentCRM module — New CRM marketing module for FluentCRM 2.8+. Syncs subscribers → mailing.contact, lists → mailing.list, tags → res.partner.category. Bidirectional subscriber/list sync, push-only tags. Uses FluentCRM custom DB tables (fc_subscribers, fc_lists, fc_tags)
  • Compatibility report link — TESTED_UP_TO version warnings now include a "Report compatibility" link that opens a pre-filled WPForms form with module name, WP4Odoo version, third-party plugin version, WordPress version, PHP version, and Odoo major version. Shown in both the global admin notice banner and per-module notices on the Modules tab. Filterable via wp4odoo_compat_report_url
  • Odoo version detectionTransport interface gains get_server_version(): ?string. JSON-RPC extracts server_version from the authenticate response; XML-RPC calls version() on /xmlrpc/2/common after auth. test_connection() now populates the version field. The AJAX handler stores the version in wp4odoo_odoo_version option for use in compat reports and diagnostics
  • Gallery images syncImage_Handler now supports product gallery images (product_image_ids_product_image_gallery). import_gallery() pulls Odoo product.image records with per-slot SHA-256 hash tracking and orphan cleanup. export_gallery() builds One2many [0, 0, {...}] tuples for push. Integrated into WC_Pull_Coordinator and WooCommerce_Module
  • Health dashboard tab — New "Health" tab in admin settings showing system status at a glance: active modules, pending queue depth, average latency, success rate, circuit breaker state, next cron run, cron warnings, compatibility warnings, and queue depth by module
  • Translatable fields for 4 modules — EDD, Events Calendar, LearnDash, and Job Manager modules now override get_translatable_fields(), enabling automatic WPML/Polylang translation pull for their primary content fields
  • Circuit breaker email notificationFailure_Notifier sends an email to the site admin when the circuit breaker opens, with failure count and a link to the health dashboard. Respects the existing cooldown interval
  • WooCommerce tax mapping — Configurable WC tax class → Odoo account.tax mapping via key-value settings. Applied per order line during push as tax_id Many2many tuples. AJAX endpoint fetches available Odoo taxes
  • WooCommerce shipping mapping — Configurable WC shipping method → Odoo delivery.carrier mapping via key-value settings. Sets carrier_id on sale.order during push. AJAX endpoint fetches available Odoo carriers
  • Separate gallery images setting — New sync_gallery_images checkbox (default on) controls gallery image push/pull independently of the featured image setting sync_product_images

Changed

  • push_entity() simplified — Removed redundant $module parameter from Module_Helpers::push_entity(). All 29 callsites across 19 trait files now use $this->id automatically
  • Circuit breaker constant publicCircuit_Breaker::OPT_CB_STATE made public; Settings_Page health tab references the constant instead of a hardcoded string
  • Form_Handler extract_normalised() — Extracted shared field iteration pipeline into a generic extract_normalised() method. Formidable, Forminator, and WPForms extractors now delegate to it; Gravity Forms uses empty_lead() instead of inline init
  • Options autoload optimization — Disabled autoload (false) on ~80 options that are only read during cron, admin, or sync operations: module settings, module mappings, webhook token, failure tracking, onboarding state, circuit breaker state, and Odoo version. Core options (connection, sync settings, log settings, module enabled flags, DB version) remain autoloaded
  • Polling safety limit warningEntity_Map_Repository::get_module_entity_mappings() and Bookly_Handler batch queries now log a warning when the 50,000-row safety cap is reached, alerting administrators that some entities may be excluded from sync
  • PHPStan level 6 — Raised static analysis from level 5 to level 6 (adds missing typehint enforcement). Global missingType.iterableValue suppression for WordPress API conformance
  • Log module filter — Expanded the log viewer module dropdown from ~20 hardcoded entries to all 33 sync modules plus 5 system modules, organized in <optgroup> sections
  • Log level i18n — Log level labels (Debug, Info, Warning, Error, Critical) in the sync settings tab are now translatable
  • Admin JS i18n — Hardcoded English strings in admin.js (server error, unknown error, completed, remove) replaced with localized strings via wp_localize_script
  • XSS defense-in-depth — Added escapeHtml() helper in admin.js for log module and level fields in the AJAX log table
  • Module toggle accessibility — Added aria-label on module enable/disable toggle switches
  • Log context columnQueryService::get_log_entries() now includes the context column in its SELECT
  • CLI confirmation promptssync run and queue retry now require interactive confirmation (skippable with --yes)