You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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:
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 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 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 TTL — Circuit_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 migrations — Database_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 validation — Dual_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 dedup — Batch_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 classification — Batch_Create_Processor now classifies invalid JSON payloads as Error_Type::Permanent (was Transient). A corrupted payload will never self-fix, so retrying wastes 2 attempts
Session detection robustness — Odoo_Client::is_session_error() now detects HTTP 401 (Odoo 17+), JSON-RPC error code 100 (session invalid), and additional keywords (session invalid, authentication failed). Reduces risk of silent re-auth failure when Odoo changes error messages
Non-idempotent method detection — Odoo_Client session retry now skips all non-idempotent methods (create, action_*, button_*, validate), not just create. Prevents unintended side effects from retrying workflow actions
Webhook HMAC logging — Webhook_Handler now logs a debug message when a webhook is received without HMAC signature (token-only auth), surfacing the weaker auth mode to site admins
Booking service sync failure propagation — Booking_Module_Base::push_to_odoo() now aborts with a Transient failure if ensure_service_synced() fails, instead of pushing a booking with an invalid Odoo service reference
DNS timeout for SSRF — Admin_Ajax::sanitize_url() now applies a 5-second default_socket_timeout during DNS resolution, preventing malicious hostnames with slow DNS from blocking the PHP thread indefinitely
Module_Registry materialization logging — Module_Registry::materialize() now catches \Throwable and logs the error instead of silently swallowing it. The module remains absent (callers handle null), but the failure is visible in logs
Settings cache invalidation — New Settings_Repository::flush_cache() method forces re-read from wp_options after external modifications (WP-CLI, concurrent cron, or another process)
Module circuit breaker concurrency — Module_Circuit_Breaker::record_module_failure() now uses advisory lock (GET_LOCK('wp4odoo_mcb_failure')) to prevent lost-update races on the shared wp_options JSON blob. is_module_available() now uses per-module probe mutex (transient + advisory lock double-check) to prevent multiple concurrent probe batches in half-open state. Probe transients cleaned in record_module_success() and reset_module()
company_id injection logging — Sync_Orchestrator::maybe_inject_company_id() now logs a warning when get_company_id() throws, instead of silently swallowing the exception
Locale mapping edge case — Translation_Service::wp_to_odoo_locale() returns 'en_US' for empty or single-character inputs instead of producing an invalid '_' locale
Documents delete result check — Documents_Module::delete_wp_data() simplified wp_delete_term() result check to true === $result (PHPStan: is_int() was unreachable for bool|WP_Error return type)
Fixed (Documentation)
ARCHITECTURE.md exclusive group priorities — Replaced ~15 stale exclusive_priority references (removed as dead code in v3.5.0) with accurate "first-registered wins" descriptions and actual registration order from Module_Registry::register_all()