Releases: PaulArgoud/WP4Odoo
Releases · PaulArgoud/WP4Odoo
3.9.1
Fixed
- WP Crowdfunding compatibility — Updated module detection from
wpneo_crowdfunding_init()function toWPCF_VERSIONconstant, meta keys from_wpneo_*to_nf_*prefix, and version constant fromSTARTER_VERSIONtoWPCF_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_BaseandMarketplace_Handler_Base, eliminating ~477 lines of duplicated code across 6 module/handler files - LMS handler template method — Added
load_course()template method toLMS_Handler_Basewith 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 intoLMS_Module_Baseviaget_lms_handler()abstract (~35 lines) - Test stub consolidation — Consolidated 14 single-constant stub files into
constants-only.phpand removed 4 empty stubs, reducingbootstrap.phprequires from 68 to 50 - PHPDoc generics — Added
array<string, mixed>return types to 11 methods inModule_BaseandSettings_Repository - Events handler base class — Extracted shared
format_event(),parse_event_from_odoo(), andformat_attendance()from 3 event handlers intoEvents_Handler_Baseabstract base - Booking handler base extended —
Jet_Booking_HandlerandJet_Appointments_Handlernow extendBooking_Handler_Base(previously standalone), bringing handler base coverage to 5 booking handlers - Events test base — New
EventsModuleTestBaseabstract test base for 3 event module tests (18 shared tests) - Booking test base — New
BookingModuleTestBaseabstract test base for 6 booking module tests (16 shared tests) - WC handler cleanup — Removed unused
$client_fnproperty fromWC_Shipping_HandlerandWC_Inventory_Handler
3.9.0
Fixed (Architecture)
safe_callback()preserves filter chain —Hook_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 UX —
Module_Registry::materialize()now records aversion_warningentry when a module's constructor throws, making the failure visible in the admin health dashboard instead of being silently swallowed - Advisory Lock reentrancy guard —
Advisory_Lock::acquire()returnstrueimmediately if the lock is already held by the current instance, preventing redundantGET_LOCK()calls - Advisory Lock destructor safety —
Advisory_Lock::__destruct()releases the lock if still held (safety net for uncaught exceptions), wrapped in try-catch so destructors never throw many2one_to_idrejects zero/negative IDs —Field_Mapper::many2one_to_id()now returnsnullfor array-path IDs ≤ 0, preventing invalid Odoo references from propagating- Logger
error_logfallback —Logger::flush_buffer()writeserror/criticalentries to PHPerror_log()when$wpdbis unavailable (late shutdown), preventing silent log loss cached_company_idmultisite invalidation —Sync_Orchestrator::maybe_inject_company_id()now comparesblog_idagainst the cached value, auto-invalidating the company_id cache whenswitch_to_blog()changes context- CLI
try/finallyonswitch_to_blog—CLI::sync(),reconcile(), andcleanup()now wrap their body intry/finallyto guaranteerestore_current_blog()is called even on exceptions - Microsecond date parsing —
Field_Mapper::odoo_date_to_wp()now triesY-m-d H:i:s.uformat first, supporting Odoo timestamps with microsecond precision - Reconciliation batch size filter —
Reconciler::reconcile()batch size is now filterable viawp4odoo_reconcile_batch_size, with ≤ 0 guard falling back to the default (200) - Translation cache flush — New
Translation_Service::flush_caches()static method clearsir.translationand active languages transients. Called from CLIcache flushcommand - Filter return type validation —
map_to_odoo()andpull_from_odoo()now validate thatapply_filters()returns an array, falling back to the pre-filter value if a third-party filter returns an unexpected type - Stale recovery clears
processed_at—Sync_Queue_Repository::recover_stale_processing()now resetsprocessed_atto NULL when recovering jobs to 'pending', restoring the semantic contract (NULL = not yet processed) and preventing phantom timestamps in monitoring dashboards - Recovery transaction try-finally —
Sync_Queue_Repository::recover_stale_processing()now wraps the SAVEPOINT/RELEASE block intry/finally, guaranteeing the transaction is committed even if a query throws - Translation buffer flush loss protection —
Translation_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 observability —
Sync_Queue_Repository::enqueue()now fireswp4odoo_enqueue_rejectedaction when a payload exceeds 1 MB, providing a hook for logging/monitoring instead of failing silently - WooCommerce
safe_callbackconsistency —wp4odoo_batch_processedhook in WooCommerce module now wrapped insafe_callback()for consistency with all other hook registrations - Compatibility report URL construction —
Admin::build_compat_report_url()now places query parameters before the#formsanchor. 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 cleanup —
deactivate()now iterates over all sites viaget_sites()when network-deactivated, preventing orphaned cron hooks on subsites (mirrors existingactivate()pattern)
Changed (Core)
- Schema cache TTL reduced to 4 hours —
Schema_Cache::CACHE_TTLchanged fromDAY_IN_SECONDS(24 h) to4 * 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 toinvalidate_stats_cache()(was causing transient thrashing on high-throughput sites). Stats cache is now only invalidated after batch processing completes inSync_Engine::run_with_lock(), where it was already called - Exclusive group admin warning —
Module_Registry::register()now records aversion_warningsentry 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 counter —Hook_Lifecyclenow tracks a static$crash_countincremented on each caught\Throwable. Exposed viaget_crash_count()for the health dashboard, making silent graceful degradation events visible to administratorsSchema_Cache::flush()in test teardown —Module_Test_Case::reset_static_caches()now callsSchema_Cache::flush()to prevent cross-test schema cache leakagePartner_Servicestatic reset in test teardown — NewPartner_Helpers::reset_partner_service()method, called fromModule_Test_Case::reset_static_caches()to prevent cross-test partner service leakage
Tests
- Added
Logger::flush_buffer()calls inMultisiteScopingTestintegration tests to account for deferred buffer writes
3.8.0
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 forevent.event, falls back tocalendar.event. Syncs events (bidirectional) and MEC Pro bookings →event.registration(push-only). Hooks:save_post_mec-events,mec_booking_completed. CPTmec-events+ custommec_eventstable. 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 viaWooCommerceEventsEventmeta. Version bounds: 1.18–2.0 - Wholesale Suite pricelist sync (WC B2B extension) — Extends existing WC B2B module with
pricelistentity type →product.pricelist. Pushes wholesale roles as Odoo pricelists, optionally assignsproperty_product_priceliston 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_Baseintermediate base class — Extracted shared logic from Events Calendar, MEC, and FooEvents into a new abstract base. Provides dual-model detection (event.event/calendar.eventfallback), 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 isolation —
Odoo_Auth::$credentials_cacheis now keyed byblog_id. Previously a single static value, causing cross-site credential leakage whenswitch_to_blog()was called within the same request flush_schema_cachenonce domain — Addedflush_schema_cachetoAdmin_Ajax::ACTION_NONCE_MAP. Was falling back to the genericwp4odoo_adminnonce domain instead of the correctwp4odoo_setup- Webhook token encryption marker —
Settings_Repositorynow prefixes encrypted webhook tokens withenc1:. 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 tracking —
Batch_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 samewp_id save_wp_datafailure now Transient —Sync_Orchestrator::pull_from_odoo()classifiessave_wp_data()returning0asError_Type::Transient(wasPermanent). WordPress save failures (DB locks, temporary constraint issues) are retryable and should not permanently fail the job
Changed (Core)
- Module settings cache —
Settings_Repository::get_module_settings()now caches results in memory, keyed byblog_id+ module ID. Avoids repeatedget_option()calls when multiple hook callbacks read the same module settings in a single request. Cache invalidated onsave_module_settings() - Circuit-breaker job deferral —
Sync_Enginenow defers jobs toscheduled_at + recovery_delaywhen 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 buffer —
Logger::log()now buffers entries in a static$write_buffer(threshold: 50). Flushes at threshold or viaregister_shutdown_function(). Reduces DB round-trips during batch processing where dozens of log calls occur per request - Shared Partner_Service —
Partner_Helpers::$shared_partner_serviceis 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_idinjection —Sync_Orchestrator::maybe_inject_company_id()now checksSchema_Cache::get_fields()before injectingcompany_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 inLoggerTest,WPAllImportModuleTest, andWebhookReliabilityTestto account for deferred buffer writes
3.7.0
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 viaForm_Field_Extractornormalizer. 3 new setting toggles - Food Ordering module: RestroPress support — Extends the existing Food Ordering module with RestroPress integration. Hooks:
restropress_complete_purchase. Extraction viaFood_Order_Extractor::extract_from_restropress(). Newsync_restropresssetting toggle - Fluent Support module — New helpdesk module extending
Helpdesk_Module_Base. Syncs tickets →helpdesk.ticket(Enterprise) withproject.taskfallback (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.orderwith 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_taskCPT and Odoofield_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.taskcase 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. Fireswp4odoo_schema_cache_flushedaction after cleanup for third-party extensibility- WP-CLI
cache flushcommand —wp wp4odoo cache flushclears the Odoo schema cache (memory + transients) on demand wp4odoo_retry_delayfilter — Retry delay inSync_Job_Tracking::handle_failure()is now filterable. Clamped to[0, MAX_RETRY_DELAY]for safety
Fixed (Architecture)
- Exponential backoff cap —
Sync_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 prevention —
Sync_Queue_Repository::recover_stale_processing()now incrementsattemptswhen recovering stale jobs. Prevents infinite recovery loops where a job that consistently crashes is recovered and reprocessed indefinitely without progressing towardmax_attempts(reverses 3.6.0 "no-increment" behavior — a job that repeatedly stalls IS progressing toward failure) - SQL injection hardening —
Sync_Queue_Repository::enqueue()andcleanup()now use$wpdb->prepare()forstatus IN (...)clauses (was safe hardcoded values, but inconsistent with the fully-prepared pattern used elsewhere) - Atomic stale recovery guard —
Sync_Engine::process_queue()now useswp_cache_add()for the stale recovery mutex (was non-atomicget_transient()/set_transient()— two concurrent crons could both pass the check) - Multisite settings cache scoping —
Settings_Repositoryinternal cache is now keyed byblog_id. On multisite,switch_to_blog()previously returned cached settings from the originating site
Fixed (Multisite)
- Credential cache flush on
switch_blog—wp4odoo.phpnow hooksswitch_blogto callOdoo_Auth::flush_credentials_cache(), preventing stale credentials from site A leaking into site B when usingswitch_to_blog()
Fixed (Modules)
- Sales_Module handler init — Moved
Portal_ManagerandPartner_Serviceinitialization fromboot()to__construct(), consistent with all other modules. Prevents potential uninitialized property access ifportal_manageris referenced beforeboot()is called
Changed
- SSL verification warning —
Odoo_JsonRPCandOdoo_XmlRPCnow log a warning whenWP4ODOO_DISABLE_SSL_VERIFYis 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 2strpos() !== falsepatterns withstr_contains()inGamiPress_Handler
CI
- Composer audit — Added
composer audit --no-devstep to GitHub Actions CI pipeline to detect known vulnerabilities in dependencies
Tests
- Updated
SyncQueueRepositoryTest— 2 tests adapted: stale recovery now assertsattemptsincrement, cleanup test matches preparedIN (%s, %s)pattern - Removed duplicate
documents-classes.phpstub require intests/bootstrap.php
3.6.0
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.orderwithpos.order.lineOne2many tuples. Strategy-based extraction viaFood_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.surveywithquestion_and_page_idsOne2many, responses →survey.user_inputwithuser_input_line_idsOne2many. Strategy-based extraction viaSurvey_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 viaLoyalty_Card_Resolver), badge types →product.template(push-only, service products). Anti-loop viaodoo_syncreference 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.bomwithbom_line_idsOne2many tuples. Cross-module entity_map resolution for WooCommerce product → Odooproduct.templateID. 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 (
documentCPT) and WP Download Manager (wpdmproCPT). Syncs documents →documents.document(bidirectional, base64 file encoding, SHA-256 change detection), folders →documents.folder(bidirectional, hierarchy viaparent_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 onwoocommerce_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 asdelivery.carrierrecords. 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.movewithmove_type=out_refund), pulls credit notes back as WC refunds. Optionally creates returnstock.pickingentries. Auto-posts credit notes viaOdoo_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_refundaccount.move). Reusesbuild_invoice_lines()for line items, supports optionalreversed_entry_idfor linking to original invoice. Filterable viawp4odoo_credit_note_datahook- Odoo_Model enum additions — 5 new cases:
StockWarehouse,StockLocation,StockMove,StockQuant,StockPickingType
Added (Architecture)
- GamiPress/myCRED
gamificationexclusive group — GamiPress and myCRED both targetloyalty.cardviaLoyalty_Card_Resolverwith(partner_id, program_id). Newgamificationexclusive 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$deferredmap;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.phpdeclares all third-party symbols (classes, functions, constants) per module.ModuleHealthManifestTestvalidates 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. UsesSyncFlowTransportmock for deterministic transport responses - Per-module circuit breaker — New
Module_Circuit_Breakerclass isolates failing modules without blocking the entire sync. Dual-level design: existing globalCircuit_Breakerhandles 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 inwp4odoo_module_cb_statesoption. Integrated intoSync_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 againstwp_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(fromModule_Base):$registered_hooks,safe_callback(),register_hook(),teardown()Translation_Accumulator(fromModule_Base):$translation_buffer,get_translatable_fields(),accumulate_pull_translation(),flush_pull_translations()Sync_Job_Tracking(fromSync_Engine):$batch_failures,$batch_successes,$module_outcomes,handle_failure(),record_module_outcome()Failure_Tracking_Settings(fromSettings_Repository): consecutive failure count and last failure email timestampUI_State_Settings(fromSettings_Repository): onboarding/checklist dismissed flags, webhook confirmation, cron healthNetwork_Settings(fromSettings_Repository): multisite network connection, site → company_id mapping
Fixed (Architecture)
- LRU cache eviction ratio —
Entity_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 recovery —
Sync_Queue_Repository::recover_stale_processing()no longer incrementsattemptswhen recovering interrupted jobs. A stale-processing job was interrupted (crash/timeout), not genuinely failed — incrementingattemptscaused premature failure atmax_attempts - Circuit breaker TTL —
Circuit_Breakertransient and DB state TTL now usesRECOVERY_DELAY × 2(600s) instead ofHOUR_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 migrations —
Database_Migration::run_migrations()now wraps each migration in aSTART TRANSACTION/COMMITblock withROLLBACKon failure, preventing partial schema changes from leaving tables in an inconsistent state - Dual accounting entity type validation —
Dual_Accounting_Module_Base::load_wp_data()now validates$entity_typeagainst 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 dedup —
Batch_Create_Processor::group_eligible_jobs()now deduplicates bywp_idwithin 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 classification —
Batch_Create_Processornow classifies invalid JSON payloads asError_Type::Permanent(wasTransient). A corrupted payload will never self-fix, so retrying wastes 2 attem...
3.5.0
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 addsblog_idcolumn towp4odoo_entity_map,wp4odoo_sync_queue, andwp4odoo_logstables. Repository layer (Entity_Map_Repository,Sync_Queue_Repository,Logger) scopes all queries byblog_id.Odoo_Clientinjectsallowed_company_idsinto kwargs context whencompany_id > 0. Network activation iterates all sites,wp_initialize_sitehook provisions new sites. Backward compatible:DEFAULT 1ensures 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 tablejet_apartment_bookingsfor 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 viamail.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_*, andwp4odoo_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_VERSIONconstant - Odoo_Model enum additions — 2 new cases:
MailActivity,MailActivityType - WP-CLI
--blog_idparameter —wp wp4odoo sync run --blog_id=3andwp wp4odoo reconcile crm contact --blog_id=3target 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)onwp4odoo_sync_queuefor efficient stale job recovery queries. Idempotent (checksSHOW INDEXbeforeALTER) - Migration 9 — Rebuild indexes with blog_id prefix — Rebuilds
idx_dedup_odooonwp4odoo_sync_queueandidx_poll_detectiononwp4odoo_entity_mapwithblog_idas leading column for correct multisite index scoping. Idempotent (checksSHOW INDEXbeforeDROP) - Migration 10 — Logs cleanup index + drop obsolete index — Adds
idx_blog_cleanup (blog_id, created_at)onwp4odoo_logsfor efficient multisite log cleanup. Drops obsoleteidx_dedup_compositefromwp4odoo_sync_queue(redundant since migration 7 addedblog_idto dedup indexes) Logger::for_channel()factory — New static factory method creates Logger instances with a sharedSettings_Repository(avoids repeatedget_option()calls when multiple loggers are created in the same request). All 12 directnew Logger()callsites migrated toLogger::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()wrapsadd_action()withsafe_callback()and tracks registered hooks.teardown()removes all tracked hooks viaremove_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 $loggerparameter. When provided (e.g. by Sync_Engine), queue depth alerts share the caller's Logger and its correlation ID. Falls back toLogger::for_channel('queue_manager')when null
Removed
exclusive_prioritydead code — Removed$exclusive_priorityproperty andget_exclusive_priority()getter fromModule_Baseand 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 transients —
Sync_Enginestale recovery transient (wp4odoo_last_stale_recovery) andQueue_Managerdepth check cooldown now includeget_current_blog_id()in their transient keys, preventing multisite sites from sharing cooldown state - Webhook token auto-migration —
Settings_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 scoping —
idx_dedup_odooandidx_poll_detectionindexes were missingblog_idas leading column, causing full-index scans in multisite installations. Migration 9 rebuilds both with correctblog_idprefix - 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()andis_private_ip()extracted from Settings_Page to shared Settings_Validator - Encryption key empty salt —
Odoo_Auth::get_encryption_key()now throwsRuntimeExceptionwhen bothAUTH_KEYandSECURE_AUTH_KEYare empty, instead of silently deriving a predictable key fromSHA256('') - Batch JSON error classification —
Batch_Create_Processornow classifies invalid JSON payloads asError_Type::Transient(retryable) instead ofPermanent, consistent withSync_Engine::process_job()behavior - AJAX exception message sanitization —
Ajax_Data_Handlers(fetch_odoo_taxes, fetch_odoo_carriers) no longer exposes raw exception messages to the browser. Errors are logged viaLogger::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_idadded toDEFAULTS_CONNECTION - Odoo_Auth multisite credential resolution —
get_credentials()falls back to network-level shared connection when site has no local URL configured. Applies site-specificcompany_idfrom network mapping.save_credentials()preservescompany_id - Sync_Engine lock scoping — Advisory lock names include
blog_id:wp4odoo_sync_{blog_id}andwp4odoo_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
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 viaOdoo_Accounting_Formatter), withdrawals →account.payment(push). Exclusive groupmarketplace. 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 groupmarketplace. 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 groupmarketplace. 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 groupecommerce_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 intomc4wp_form_subscribedandmc4wp_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 viahr.employeelookup. 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) orbom_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_mappingssettings 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 alerting —
Queue_Managermonitors pending job count and fireswp4odoo_queue_depth_warning(≥ 1 000 jobs) andwp4odoo_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_Jobreadonly 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 returnsQueue_Job[]instead of rawstdClassarrays - Advisory_Lock utility class — Reusable MySQL advisory lock wrapper (
acquire(),release(),is_held()) consolidating 3 duplicate implementations acrossSync_Engine,Partner_Service, andPush_Lock
Changed
- Queue_Manager injectable —
Queue_Manageris now instantiable with an optionalSync_Queue_Repositorydependency.Module_Baseexposesqueue()accessor andset_queue_manager()setter. Module push/pull operations use the injectable instance. Static API preserved for backward compatibility - Chunked DELETE in cleanup —
Sync_Queue_Repository::cleanup()andLogger::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 acrossCLI,CLI_Queue_Commands, andCLI_Module_Commandswrapped in__()for translation support - Injectable transport for Odoo_Client —
Odoo_Clientconstructor now accepts optional?Transportand?Settings_Repositoryparameters, enabling test doubles without monkey-patching and improving DI testability - Standardized Logger construction —
Loggernow consistently receivesSettings_Repositoryacross all construction sites (Odoo_Client,Translation_Service,Ajax_Monitor_Handlers), eliminating barenew Logger()calls - Webhook_Handler dependency injection —
Webhook_Handlernow receivesModule_Registryvia constructor instead of resolving the plugin singleton internally - Advisory lock on batch creates —
push_batch_creates()acquires a per-model advisory lock (wp4odoo_batch_{module}_{model}) to prevent concurrent batch creates from producing duplicates - Settings write-time validation —
Settings_Repositorygainssave_sync_settings()andsave_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_Helpersinto 4 focused sub-traits:Partner_Helpers,Accounting_Helpers,Dependency_Helpers,Sync_Helpers.Module_Helpersnow composes all 4 viausefor backward compatibility
Fixed
- Exclusive group priority bug —
Module_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 fromForm_Handler(514 → 97 LOC) into a strategy-basedForm_Field_Extractorclass with registered closures per form plugin - WC_Translation_Accumulator — Extracted translation accumulation logic from
WC_Pull_Coordinator(723 → 520 LOC) into a focusedWC_Translation_Accumulatorclass (289 LOC) - Translation strategies — Extracted Odoo version-specific push/pull logic from
Translation_Service(768 → 629 LOC) intoTranslation_Strategy_Modern(Odoo 16+) andTranslation_Strategy_Legacy(Odoo 14–15) behind aTranslation_Strategyinterface - Sync_Orchestrator trait — Extracted
push_to_odoo(),push_batch_creates(), andpull_from_odoo()fromModule_Base(1 245 → 921 LOC) into aSync_Orchestratortrait (344 LOC) - Batch_Create_Processor — Extracted batch create pipeline from
Sync_Engine(811 → 731 LOC) into a dedicatedBatch_Create_Processorclass (208 LOC) with injected dependencies - Test base classes — Extracted
MembershipModuleTestBase(30 shared tests) andLMSModuleTestBase(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
Added
- Queue observability hook — New
wp4odoo_job_processedaction 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_Registryenforces 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 hardcodedError_Type::Transientin 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_Resolvertrait (~75 LOC deduplication) - CLI trait extraction — Extracted queue subcommands (stats, list, retry, cleanup, cancel) and module subcommands (list, enable, disable) from
CLIclass intoCLI_Queue_CommandsandCLI_Module_Commandstraits for testability and separation of concerns - Rate_Limiter extraction — Extracted transient-based rate limiting from
Webhook_Handlerinto a standaloneRate_Limiterclass, 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 thepush_entity()helper, ensuring consistentshould_sync()guards and automatic create/update determination via mapping lookup
Fixed
- uninstall.php — Added missing
wp4odoo_log_cleanupcron event cleanup (previously onlywp4odoo_scheduled_syncwas cleared)
3.2.5
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 viawp4odoo_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 viawp4odoo_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 processing —
Sync_Enginenow 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 lock —
push_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 polling —
poll_entity_changes()now uses targeted entity_map loading (IN (wp_ids)) instead of loading all rows, and detects deletions vialast_polled_attimestamps instead of in-memory set comparison. New DB migration addslast_polled_atcolumn toentity_map
Changed
- Sync_Engine method extraction — Extracted
should_continue_batching()andprocess_fetched_batch()fromrun_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 withlast_polled_at). Module_Base uses all 3 viausestatements - CRM_Module error classification —
push_to_odoo()override now catches\RuntimeExceptionand classifies errors viaError_Classification::classify_exception()instead of treating all failures as transient - Contact_Refiner resilience — All refinement methods (
refine_name,refine_country,refine_state) now catch\Throwableand return unmodified data on failure, preventing a single refinement error from blocking the entire sync - Batch creates N+1 fix —
push_batch_creates()now passes the pre-resolved$moduleinstance toprocess_single_job()instead of re-resolving fromModule_Registryfor each job in the batch - Field_Mapper date format cache —
convert_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 → transientPushDedupLockTest(6 tests) — Push_Lock trait: advisory lock acquire/release, lock on create path, no lock on update path, lock timeout → transient errorDatabaseMigrationTest(8 tests) — Migration 6:last_polled_atcolumn addition, index creation, idempotency
3.2.0
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 failure —
Sync_Engine::process_batch_creates()added jobs to$claimed_jobsbefore JSON validation, causinghandle_failure()to be called twice (once for invalid JSON, once for batch error). Moved append after validation - SSRF bypass via DNS failure —
is_safe_url()returnedtruewhengethostbyname()failed (returns the input on DNS failure), allowing URLs with unresolvable hostnames to bypass SSRF protection. Now returnsfalse - Queue health metrics cache leak —
invalidate_stats_cache()clearedwp4odoo_queue_statsbut notwp4odoo_queue_health, leaving stale health metrics - Stale recovery ordering —
recover_stale_processing()ran afterfetch_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_callaction, making retry calls invisible to monitors - MySQL 5.7 compat —
@@in_transactionsession variable query could produce a visible error on MySQL 5.7 (which lacks this variable). Wrapped withsuppress_errors() - Dual accounting delete —
resolve_accounting_model()was skipped fordeleteactions, causing delete calls to target the wrong Odoo model when OCAdonation.donationwas active - Helpdesk exclusive group/priority —
Helpdesk_Module_Baseused method overrides instead of properties for$exclusive_groupand$exclusive_priority, inconsistent with all other intermediate bases. Converted to properties - Undefined
$jobsvariable —Sync_Engine::process_queue()could reference undefined$jobsif the try block threw before assignment - Partner email normalization —
Partner_Service::get_or_create_batch()now trims and lowercases emails before Odoo lookup, preventing duplicate partners from case mismatches - Reconciler client hoisting —
Reconcilerresolved the Odoo client inside the per-entity loop instead of once before it - Logger context truncation —
truncate_context()JSON-encoded the full array then truncated the string, producing invalid JSON. Now truncates the array first, then encodes - Empty encryption key warning —
Odoo_Authnow logs a warning viaerror_log()when the encryption key is empty, aiding diagnosis of misconfigured installations - CLI
--formatvalidation —queue statsandqueue listsubcommands now reject unsupported--formatvalues with a clear error - Ecwid cron orphan on deactivation —
wp4odoo_ecwid_pollcron event was cleared on uninstall but not on plugin deactivation, leaving an orphaned cron entry. Added todeactivate() - 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_Handlerclass. Version-adaptive API:stock.quant+action_apply_inventory()for Odoo 16+,stock.change.product.qtywizard 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. ExtendsLMS_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 detection —
Transportinterface gainsget_server_version(): ?string. JSON-RPC extractsserver_versionfrom the authenticate response; XML-RPC callsversion()on/xmlrpc/2/commonafter auth.test_connection()now populates theversionfield. The AJAX handler stores the version inwp4odoo_odoo_versionoption for use in compat reports and diagnostics - Gallery images sync —
Image_Handlernow supports product gallery images (product_image_ids↔_product_image_gallery).import_gallery()pulls Odooproduct.imagerecords 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 notification —
Failure_Notifiersends 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.taxmapping via key-value settings. Applied per order line during push astax_idMany2many tuples. AJAX endpoint fetches available Odoo taxes - WooCommerce shipping mapping — Configurable WC shipping method → Odoo
delivery.carriermapping via key-value settings. Setscarrier_idonsale.orderduring push. AJAX endpoint fetches available Odoo carriers - Separate gallery images setting — New
sync_gallery_imagescheckbox (default on) controls gallery image push/pull independently of the featured image settingsync_product_images
Changed
push_entity()simplified — Removed redundant$moduleparameter fromModule_Helpers::push_entity(). All 29 callsites across 19 trait files now use$this->idautomatically- Circuit breaker constant public —
Circuit_Breaker::OPT_CB_STATEmade public;Settings_Pagehealth tab references the constant instead of a hardcoded string - Form_Handler
extract_normalised()— Extracted shared field iteration pipeline into a genericextract_normalised()method. Formidable, Forminator, and WPForms extractors now delegate to it; Gravity Forms usesempty_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 warning —
Entity_Map_Repository::get_module_entity_mappings()andBookly_Handlerbatch 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.iterableValuesuppression 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 viawp_localize_script - XSS defense-in-depth — Added
escapeHtml()helper inadmin.jsfor logmoduleandlevelfields in the AJAX log table - Module toggle accessibility — Added
aria-labelon module enable/disable toggle switches - Log context column —
QueryService::get_log_entries()now includes thecontextcolumn in its SELECT - CLI confirmation prompts —
sync runandqueue retrynow require interactive confirmation (skippable with--yes)