File: /var/www/html/wp-content/mu-plugins/ethnic-odoo-crm-sync.php
<?php
/**
* Plugin Name: Ethnic Odoo CRM Sync
* Description: Shared Odoo CRM settings and lead-sync helpers for website lead channels.
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'ETHNIC_ODOO_CRM_SETTINGS_OPTION_KEY' ) ) {
define( 'ETHNIC_ODOO_CRM_SETTINGS_OPTION_KEY', '_ethnic_odoo_crm_settings' );
}
if ( ! function_exists( 'ethnic_odoo_crm_legacy_resources_option_key' ) ) {
function ethnic_odoo_crm_legacy_resources_option_key() {
return '_ethnic_resources_odoo_settings';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_page_slug' ) ) {
function ethnic_odoo_crm_page_slug() {
return 'ethnic-odoo-crm-sync';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_legacy_resources_page_slug' ) ) {
function ethnic_odoo_crm_legacy_resources_page_slug() {
return 'ethnic-resources-odoo-sync';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_channels' ) ) {
function ethnic_odoo_crm_channels() {
return array(
'resource' => array(
'label' => 'Resource Downloads',
'description' => 'Send downloadable resource submissions from the Resources hub into Odoo CRM.',
'enabled' => 1,
'lead_prefix' => 'Resource Download',
'source_label' => 'Website Resources',
'lead_type' => 'lead',
),
'proposal' => array(
'label' => 'Proposal Requests',
'description' => 'Send structured Request a Proposal submissions, including project context and files, into Odoo CRM.',
'enabled' => 1,
'lead_prefix' => 'Proposal Request',
'source_label' => 'Website Proposal Requests',
'lead_type' => 'opportunity',
),
'contact' => array(
'label' => 'Contact Form',
'description' => 'Send enquiries submitted through the shared website contact form into Odoo CRM.',
'enabled' => 1,
'lead_prefix' => 'Contact Inquiry',
'source_label' => 'Website Contact Form',
'lead_type' => 'lead',
),
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_normalize_base_url' ) ) {
function ethnic_odoo_crm_normalize_base_url( $url ) {
$url = is_scalar( $url ) ? trim( (string) $url ) : '';
if ( '' === $url ) {
return '';
}
$url = preg_replace( '#/web/?$#', '', $url );
$url = untrailingslashit( $url );
if ( ! preg_match( '#^https?://#i', $url ) ) {
$url = 'https://' . ltrim( $url, '/' );
}
return esc_url_raw( $url );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_default_settings' ) ) {
function ethnic_odoo_crm_default_settings() {
$channels = array();
foreach ( ethnic_odoo_crm_channels() as $channel => $definition ) {
$channels[ $channel ] = array(
'enabled' => ! empty( $definition['enabled'] ) ? 1 : 0,
'lead_prefix' => isset( $definition['lead_prefix'] ) ? (string) $definition['lead_prefix'] : '',
'source_label' => isset( $definition['source_label'] ) ? (string) $definition['source_label'] : '',
'lead_type' => isset( $definition['lead_type'] ) ? (string) $definition['lead_type'] : 'lead',
);
}
return array(
'enabled' => 0,
'base_url' => '',
'database' => '',
'username' => '',
'password' => '',
'team_id' => 0,
'user_id' => 0,
'menu_id' => 0,
'action_id' => 0,
'cids' => '',
'last_connection_status' => '',
'last_connection_message'=> '',
'last_checked_at' => '',
'last_uid' => 0,
'channels' => $channels,
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_settings' ) ) {
function ethnic_odoo_crm_settings() {
$defaults = ethnic_odoo_crm_default_settings();
$stored = get_option( ETHNIC_ODOO_CRM_SETTINGS_OPTION_KEY, array() );
$settings = is_array( $stored ) ? wp_parse_args( $stored, $defaults ) : $defaults;
$settings['base_url'] = ethnic_odoo_crm_normalize_base_url( $settings['base_url'] );
$settings['channels'] = isset( $settings['channels'] ) && is_array( $settings['channels'] ) ? $settings['channels'] : array();
foreach ( ethnic_odoo_crm_channels() as $channel => $definition ) {
$defaults_for_channel = $defaults['channels'][ $channel ];
$settings['channels'][ $channel ] = isset( $settings['channels'][ $channel ] ) && is_array( $settings['channels'][ $channel ] )
? wp_parse_args( $settings['channels'][ $channel ], $defaults_for_channel )
: $defaults_for_channel;
}
return $settings;
}
}
if ( ! function_exists( 'ethnic_odoo_crm_channel_settings' ) ) {
function ethnic_odoo_crm_channel_settings( $channel, $settings = null ) {
$channel = sanitize_key( (string) $channel );
$settings = is_array( $settings ) ? wp_parse_args( $settings, ethnic_odoo_crm_default_settings() ) : ethnic_odoo_crm_settings();
$channels = ethnic_odoo_crm_channels();
if ( empty( $channels[ $channel ] ) ) {
return array();
}
$channel_settings = isset( $settings['channels'][ $channel ] ) && is_array( $settings['channels'][ $channel ] )
? wp_parse_args( $settings['channels'][ $channel ], $channels[ $channel ] )
: $channels[ $channel ];
return array(
'channel' => $channel,
'channel_label' => isset( $channels[ $channel ]['label'] ) ? (string) $channels[ $channel ]['label'] : ucfirst( $channel ),
'enabled' => ! empty( $settings['enabled'] ) && ! empty( $channel_settings['enabled'] ) ? 1 : 0,
'base_url' => $settings['base_url'],
'database' => $settings['database'],
'username' => $settings['username'],
'password' => $settings['password'],
'team_id' => absint( $settings['team_id'] ),
'user_id' => absint( $settings['user_id'] ),
'menu_id' => absint( $settings['menu_id'] ),
'action_id' => absint( $settings['action_id'] ),
'cids' => isset( $settings['cids'] ) ? sanitize_text_field( (string) $settings['cids'] ) : '',
'last_connection_status' => isset( $settings['last_connection_status'] ) ? sanitize_key( (string) $settings['last_connection_status'] ) : '',
'last_connection_message'=> isset( $settings['last_connection_message'] ) ? sanitize_text_field( (string) $settings['last_connection_message'] ) : '',
'last_checked_at' => isset( $settings['last_checked_at'] ) ? sanitize_text_field( (string) $settings['last_checked_at'] ) : '',
'last_uid' => isset( $settings['last_uid'] ) ? absint( $settings['last_uid'] ) : 0,
'lead_prefix' => isset( $channel_settings['lead_prefix'] ) ? sanitize_text_field( (string) $channel_settings['lead_prefix'] ) : '',
'source_label' => isset( $channel_settings['source_label'] ) ? sanitize_text_field( (string) $channel_settings['source_label'] ) : '',
'lead_type' => 'opportunity' === ( isset( $channel_settings['lead_type'] ) ? sanitize_key( (string) $channel_settings['lead_type'] ) : '' ) ? 'opportunity' : 'lead',
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_credentials_present' ) ) {
function ethnic_odoo_crm_credentials_present( $settings = null ) {
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
return ! empty( $settings['base_url'] ) && ! empty( $settings['database'] ) && ! empty( $settings['username'] ) && ! empty( $settings['password'] );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_sanitize_settings' ) ) {
function ethnic_odoo_crm_sanitize_settings( $posted_settings, $existing_settings = array() ) {
$defaults = ethnic_odoo_crm_default_settings();
$existing = is_array( $existing_settings ) ? wp_parse_args( $existing_settings, $defaults ) : $defaults;
$posted = is_array( $posted_settings ) ? $posted_settings : array();
$password = isset( $posted['password'] ) ? trim( (string) $posted['password'] ) : '';
$settings = array(
'enabled' => ! empty( $posted['enabled'] ) ? 1 : 0,
'base_url' => ethnic_odoo_crm_normalize_base_url( isset( $posted['base_url'] ) ? $posted['base_url'] : '' ),
'database' => sanitize_text_field( isset( $posted['database'] ) ? (string) $posted['database'] : '' ),
'username' => sanitize_text_field( isset( $posted['username'] ) ? (string) $posted['username'] : '' ),
'password' => '' !== $password ? $password : ( isset( $existing['password'] ) ? (string) $existing['password'] : '' ),
'team_id' => absint( isset( $posted['team_id'] ) ? $posted['team_id'] : 0 ),
'user_id' => absint( isset( $posted['user_id'] ) ? $posted['user_id'] : 0 ),
'menu_id' => absint( isset( $posted['menu_id'] ) ? $posted['menu_id'] : 0 ),
'action_id' => absint( isset( $posted['action_id'] ) ? $posted['action_id'] : 0 ),
'cids' => sanitize_text_field( isset( $posted['cids'] ) ? (string) $posted['cids'] : '' ),
'last_connection_status' => isset( $existing['last_connection_status'] ) ? sanitize_key( (string) $existing['last_connection_status'] ) : '',
'last_connection_message'=> isset( $existing['last_connection_message'] ) ? sanitize_text_field( (string) $existing['last_connection_message'] ) : '',
'last_checked_at' => isset( $existing['last_checked_at'] ) ? sanitize_text_field( (string) $existing['last_checked_at'] ) : '',
'last_uid' => isset( $existing['last_uid'] ) ? absint( $existing['last_uid'] ) : 0,
'channels' => array(),
);
foreach ( ethnic_odoo_crm_channels() as $channel => $definition ) {
$posted_channel = isset( $posted['channels'][ $channel ] ) && is_array( $posted['channels'][ $channel ] ) ? $posted['channels'][ $channel ] : array();
$existing_channel = isset( $existing['channels'][ $channel ] ) && is_array( $existing['channels'][ $channel ] ) ? $existing['channels'][ $channel ] : array();
$default_channel = array(
'enabled' => ! empty( $definition['enabled'] ) ? 1 : 0,
'lead_prefix' => isset( $definition['lead_prefix'] ) ? (string) $definition['lead_prefix'] : '',
'source_label' => isset( $definition['source_label'] ) ? (string) $definition['source_label'] : '',
'lead_type' => isset( $definition['lead_type'] ) ? (string) $definition['lead_type'] : 'lead',
);
$current_channel = wp_parse_args( $existing_channel, $default_channel );
$lead_type = isset( $posted_channel['lead_type'] ) ? sanitize_key( (string) $posted_channel['lead_type'] ) : $current_channel['lead_type'];
$settings['channels'][ $channel ] = array(
'enabled' => ! empty( $posted_channel['enabled'] ) ? 1 : 0,
'lead_prefix' => sanitize_text_field( isset( $posted_channel['lead_prefix'] ) ? (string) $posted_channel['lead_prefix'] : $current_channel['lead_prefix'] ),
'source_label' => sanitize_text_field( isset( $posted_channel['source_label'] ) ? (string) $posted_channel['source_label'] : $current_channel['source_label'] ),
'lead_type' => 'opportunity' === $lead_type ? 'opportunity' : 'lead',
);
}
return $settings;
}
}
if ( ! function_exists( 'ethnic_odoo_crm_maybe_migrate_legacy_settings' ) ) {
function ethnic_odoo_crm_maybe_migrate_legacy_settings() {
$stored = get_option( ETHNIC_ODOO_CRM_SETTINGS_OPTION_KEY, null );
if ( is_array( $stored ) && ! empty( $stored ) ) {
return;
}
$legacy = get_option( ethnic_odoo_crm_legacy_resources_option_key(), array() );
if ( ! is_array( $legacy ) || empty( $legacy ) ) {
return;
}
$settings = ethnic_odoo_crm_default_settings();
$settings['enabled'] = ! empty( $legacy['enabled'] ) ? 1 : 0;
$settings['base_url'] = ethnic_odoo_crm_normalize_base_url( isset( $legacy['base_url'] ) ? $legacy['base_url'] : '' );
$settings['database'] = isset( $legacy['database'] ) ? sanitize_text_field( (string) $legacy['database'] ) : '';
$settings['username'] = isset( $legacy['username'] ) ? sanitize_text_field( (string) $legacy['username'] ) : '';
$settings['password'] = isset( $legacy['password'] ) ? (string) $legacy['password'] : '';
$settings['team_id'] = isset( $legacy['team_id'] ) ? absint( $legacy['team_id'] ) : 0;
$settings['user_id'] = isset( $legacy['user_id'] ) ? absint( $legacy['user_id'] ) : 0;
$settings['last_connection_status'] = isset( $legacy['last_connection_status'] ) ? sanitize_key( (string) $legacy['last_connection_status'] ) : '';
$settings['last_connection_message']= isset( $legacy['last_connection_message'] ) ? sanitize_text_field( (string) $legacy['last_connection_message'] ) : '';
$settings['last_checked_at'] = isset( $legacy['last_checked_at'] ) ? sanitize_text_field( (string) $legacy['last_checked_at'] ) : '';
$settings['last_uid'] = isset( $legacy['last_uid'] ) ? absint( $legacy['last_uid'] ) : 0;
$settings['channels']['resource'] = array(
'enabled' => ! empty( $legacy['enabled'] ) ? 1 : 0,
'lead_prefix' => isset( $legacy['lead_prefix'] ) ? sanitize_text_field( (string) $legacy['lead_prefix'] ) : $settings['channels']['resource']['lead_prefix'],
'source_label' => isset( $legacy['source_label'] ) ? sanitize_text_field( (string) $legacy['source_label'] ) : $settings['channels']['resource']['source_label'],
'lead_type' => 'lead',
);
update_option( ETHNIC_ODOO_CRM_SETTINGS_OPTION_KEY, $settings, false );
}
add_action( 'init', 'ethnic_odoo_crm_maybe_migrate_legacy_settings', 6 );
}
if ( ! function_exists( 'ethnic_odoo_crm_mirror_resource_settings' ) ) {
function ethnic_odoo_crm_mirror_resource_settings( $settings = null ) {
$all_settings = is_array( $settings ) ? wp_parse_args( $settings, ethnic_odoo_crm_default_settings() ) : ethnic_odoo_crm_settings();
$resource = ethnic_odoo_crm_channel_settings( 'resource', $all_settings );
$legacy = array(
'enabled' => ! empty( $resource['enabled'] ) ? 1 : 0,
'base_url' => $all_settings['base_url'],
'database' => $all_settings['database'],
'username' => $all_settings['username'],
'password' => $all_settings['password'],
'lead_prefix' => $resource['lead_prefix'],
'source_label' => $resource['source_label'],
'team_id' => absint( $all_settings['team_id'] ),
'user_id' => absint( $all_settings['user_id'] ),
'last_connection_status' => isset( $all_settings['last_connection_status'] ) ? sanitize_key( (string) $all_settings['last_connection_status'] ) : '',
'last_connection_message'=> isset( $all_settings['last_connection_message'] ) ? sanitize_text_field( (string) $all_settings['last_connection_message'] ) : '',
'last_checked_at' => isset( $all_settings['last_checked_at'] ) ? sanitize_text_field( (string) $all_settings['last_checked_at'] ) : '',
'last_uid' => isset( $all_settings['last_uid'] ) ? absint( $all_settings['last_uid'] ) : 0,
);
update_option( ethnic_odoo_crm_legacy_resources_option_key(), $legacy, false );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_require_client' ) ) {
function ethnic_odoo_crm_require_client() {
if ( ! class_exists( 'IXR_Base64' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-base64.php';
}
if ( ! class_exists( 'IXR_Date' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-date.php';
}
if ( ! class_exists( 'IXR_Value' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-value.php';
}
if ( ! class_exists( 'IXR_Client' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-client.php';
}
if ( ! class_exists( 'IXR_Request' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-request.php';
}
if ( ! class_exists( 'IXR_Message' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-message.php';
}
if ( ! class_exists( 'IXR_Error' ) ) {
require_once ABSPATH . WPINC . '/IXR/class-IXR-error.php';
}
if ( ! class_exists( 'WP_HTTP_IXR_Client' ) ) {
require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php';
}
}
}
if ( ! function_exists( 'ethnic_odoo_crm_client' ) ) {
function ethnic_odoo_crm_client( $endpoint, $timeout = 12 ) {
ethnic_odoo_crm_require_client();
return new WP_HTTP_IXR_Client( $endpoint, false, false, $timeout );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_common_endpoint' ) ) {
function ethnic_odoo_crm_common_endpoint( $settings = null ) {
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
return untrailingslashit( (string) $settings['base_url'] ) . '/xmlrpc/2/common';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_object_endpoint' ) ) {
function ethnic_odoo_crm_object_endpoint( $settings = null ) {
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
return untrailingslashit( (string) $settings['base_url'] ) . '/xmlrpc/2/object';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_authenticate' ) ) {
function ethnic_odoo_crm_authenticate( $settings = null ) {
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
if ( ! ethnic_odoo_crm_credentials_present( $settings ) ) {
return new WP_Error( 'ethnic_odoo_crm_missing_credentials', 'Add the Odoo URL, database, username, and password/API key before enabling CRM sync.' );
}
$client = ethnic_odoo_crm_client( ethnic_odoo_crm_common_endpoint( $settings ) );
$called = $client->query(
'authenticate',
(string) $settings['database'],
(string) $settings['username'],
(string) $settings['password'],
array()
);
if ( ! $called ) {
return new WP_Error(
'ethnic_odoo_crm_auth_failed',
$client->isError() ? $client->getErrorMessage() : 'Odoo authentication failed.'
);
}
$uid = absint( $client->getResponse() );
if ( $uid <= 0 ) {
return new WP_Error( 'ethnic_odoo_crm_auth_invalid', 'Odoo did not return a valid authenticated user ID.' );
}
return $uid;
}
}
if ( ! function_exists( 'ethnic_odoo_crm_execute' ) ) {
function ethnic_odoo_crm_execute( $settings, $uid, $model, $method, $args = array(), $kwargs = array() ) {
$client = ethnic_odoo_crm_client( ethnic_odoo_crm_object_endpoint( $settings ) );
$called = $client->query(
'execute_kw',
(string) $settings['database'],
(int) $uid,
(string) $settings['password'],
(string) $model,
(string) $method,
(array) $args,
(array) $kwargs
);
if ( ! $called ) {
return new WP_Error(
'ethnic_odoo_crm_execute_failed',
$client->isError() ? $client->getErrorMessage() : 'Odoo request failed while creating the CRM lead.'
);
}
return $client->getResponse();
}
}
if ( ! function_exists( 'ethnic_odoo_crm_test_connection' ) ) {
function ethnic_odoo_crm_test_connection( $settings ) {
if ( empty( $settings['enabled'] ) ) {
return array(
'status' => 'disabled',
'message' => 'Odoo CRM sync is saved but currently disabled.',
'uid' => 0,
);
}
if ( ! ethnic_odoo_crm_credentials_present( $settings ) ) {
return array(
'status' => 'warning',
'message' => 'Settings saved. Add the database, username, and password/API key to complete the Odoo connection.',
'uid' => 0,
);
}
$uid = ethnic_odoo_crm_authenticate( $settings );
if ( is_wp_error( $uid ) ) {
return array(
'status' => 'error',
'message' => 'Settings saved, but Odoo authentication failed: ' . $uid->get_error_message(),
'uid' => 0,
);
}
return array(
'status' => 'success',
'message' => 'Connected to Odoo CRM successfully. Enabled website channels can now create CRM leads.',
'uid' => (int) $uid,
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_create_lead' ) ) {
function ethnic_odoo_crm_create_lead( $payload, $settings = null ) {
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
if ( empty( $settings['enabled'] ) ) {
return new WP_Error( 'ethnic_odoo_crm_disabled', 'Odoo CRM sync is currently disabled in settings.' );
}
if ( ! ethnic_odoo_crm_credentials_present( $settings ) ) {
return new WP_Error( 'ethnic_odoo_crm_not_configured', 'Complete the Odoo CRM settings before syncing website leads.' );
}
$uid = ethnic_odoo_crm_authenticate( $settings );
if ( is_wp_error( $uid ) ) {
return $uid;
}
$lead_id = ethnic_odoo_crm_execute(
$settings,
$uid,
'crm.lead',
'create',
array(
array( (array) $payload ),
),
array()
);
if ( is_wp_error( $lead_id ) ) {
return $lead_id;
}
$lead_id = absint( $lead_id );
if ( $lead_id <= 0 ) {
return new WP_Error( 'ethnic_odoo_crm_invalid_lead', 'Odoo did not return a valid CRM lead ID.' );
}
return $lead_id;
}
}
if ( ! function_exists( 'ethnic_odoo_crm_update_lead' ) ) {
function ethnic_odoo_crm_update_lead( $lead_id, $payload, $settings = null ) {
$lead_id = absint( $lead_id );
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
if ( $lead_id <= 0 ) {
return new WP_Error( 'ethnic_odoo_crm_invalid_lead', 'A valid Odoo CRM lead ID is required before updating the lead.' );
}
if ( empty( $settings['enabled'] ) ) {
return new WP_Error( 'ethnic_odoo_crm_disabled', 'Odoo CRM sync is currently disabled in settings.' );
}
if ( ! ethnic_odoo_crm_credentials_present( $settings ) ) {
return new WP_Error( 'ethnic_odoo_crm_not_configured', 'Complete the Odoo CRM settings before syncing website leads.' );
}
$uid = ethnic_odoo_crm_authenticate( $settings );
if ( is_wp_error( $uid ) ) {
return $uid;
}
$result = ethnic_odoo_crm_execute(
$settings,
$uid,
'crm.lead',
'write',
array(
array( $lead_id ),
array( (array) $payload ),
),
array()
);
if ( is_wp_error( $result ) ) {
return $result;
}
if ( false === $result ) {
return new WP_Error( 'ethnic_odoo_crm_update_failed', 'Odoo did not confirm the CRM lead update.' );
}
return true;
}
}
if ( ! function_exists( 'ethnic_odoo_crm_lead_admin_url' ) ) {
function ethnic_odoo_crm_lead_admin_url( $lead_id, $settings = null ) {
$lead_id = absint( $lead_id );
$settings = is_array( $settings ) ? $settings : ethnic_odoo_crm_settings();
if ( $lead_id <= 0 || empty( $settings['base_url'] ) ) {
return '';
}
$args = array(
'id' => $lead_id,
'model' => 'crm.lead',
'view_type' => 'form',
);
if ( ! empty( $settings['cids'] ) ) {
$args['cids'] = sanitize_text_field( (string) $settings['cids'] );
}
if ( ! empty( $settings['menu_id'] ) ) {
$args['menu_id'] = absint( $settings['menu_id'] );
}
if ( ! empty( $settings['action_id'] ) ) {
$args['action'] = absint( $settings['action_id'] );
}
return untrailingslashit( (string) $settings['base_url'] ) . '/web#' . http_build_query( $args, '', '&', PHP_QUERY_RFC3986 );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_sync_status_label' ) ) {
function ethnic_odoo_crm_sync_status_label( $status ) {
$labels = array(
'synced' => 'Synced',
'failed' => 'Failed',
'not-configured' => 'Not configured',
'disabled' => 'Disabled',
'syncing' => 'Syncing',
);
return isset( $labels[ $status ] ) ? $labels[ $status ] : 'Pending';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_sync_badge_html' ) ) {
function ethnic_odoo_crm_sync_badge_html( $status ) {
$status = sanitize_key( (string) $status );
$map = array(
'synced' => array( '#dfffea', '#12733d' ),
'failed' => array( '#ffe6e6', '#9a1d1d' ),
'not-configured' => array( '#fff5d6', '#8a5a00' ),
'disabled' => array( '#eef2ff', '#44557d' ),
'syncing' => array( '#eef4ff', '#3457a0' ),
'pending' => array( '#eef2ff', '#44557d' ),
);
$item = isset( $map[ $status ] ) ? $map[ $status ] : $map['pending'];
return sprintf(
'<span style="display:inline-flex;align-items:center;min-height:26px;padding:4px 10px;border-radius:999px;background:%1$s;color:%2$s;font-weight:700;font-size:12px;line-height:1.2;">%3$s</span>',
esc_attr( $item[0] ),
esc_attr( $item[1] ),
esc_html( ethnic_odoo_crm_sync_status_label( $status ) )
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_admin_notice_key' ) ) {
function ethnic_odoo_crm_admin_notice_key() {
return 'ethnic_odoo_crm_admin_notice';
}
}
if ( ! function_exists( 'ethnic_odoo_crm_set_admin_notice' ) ) {
function ethnic_odoo_crm_set_admin_notice( $type, $message ) {
set_transient(
ethnic_odoo_crm_admin_notice_key(),
array(
'type' => sanitize_key( (string) $type ),
'message' => sanitize_text_field( (string) $message ),
),
60
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_render_admin_notice' ) ) {
function ethnic_odoo_crm_render_admin_notice() {
$notice = get_transient( ethnic_odoo_crm_admin_notice_key() );
if ( ! is_array( $notice ) || empty( $notice['message'] ) ) {
return;
}
delete_transient( ethnic_odoo_crm_admin_notice_key() );
$type = ! empty( $notice['type'] ) ? $notice['type'] : 'info';
if ( ! in_array( $type, array( 'success', 'error', 'warning', 'info' ), true ) ) {
$type = 'info';
}
?>
<div class="notice notice-<?php echo esc_attr( 'warning' === $type ? 'warning' : $type ); ?> is-dismissible">
<p><?php echo esc_html( $notice['message'] ); ?></p>
</div>
<?php
}
add_action( 'admin_notices', 'ethnic_odoo_crm_render_admin_notice' );
}
if ( ! function_exists( 'ethnic_odoo_crm_page_url' ) ) {
function ethnic_odoo_crm_page_url( $args = array() ) {
$args = wp_parse_args(
(array) $args,
array(
'post_type' => 'page',
'page' => ethnic_odoo_crm_page_slug(),
)
);
return add_query_arg( $args, admin_url( 'edit.php' ) );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_connection_badge_html' ) ) {
function ethnic_odoo_crm_connection_badge_html( $status ) {
$status = sanitize_key( (string) $status );
$map = array(
'success' => array( 'Connected', '#dfffea', '#12733d' ),
'error' => array( 'Needs attention', '#ffe6e6', '#9a1d1d' ),
'warning' => array( 'Incomplete', '#fff5d6', '#8a5a00' ),
'disabled' => array( 'Disabled', '#eef2ff', '#44557d' ),
);
$item = isset( $map[ $status ] ) ? $map[ $status ] : array( 'Not tested', '#eef2ff', '#44557d' );
return sprintf(
'<span style="display:inline-flex;align-items:center;gap:8px;min-height:34px;padding:8px 14px;border-radius:999px;background:%1$s;color:%2$s;font-weight:700;">%3$s</span>',
esc_attr( $item[1] ),
esc_attr( $item[2] ),
esc_html( $item[0] )
);
}
}
if ( ! function_exists( 'ethnic_odoo_crm_remove_legacy_resources_submenu' ) ) {
function ethnic_odoo_crm_remove_legacy_resources_submenu() {
remove_submenu_page( 'edit.php?post_type=page', ethnic_odoo_crm_legacy_resources_page_slug() );
}
add_action( 'admin_menu', 'ethnic_odoo_crm_remove_legacy_resources_submenu', 999 );
}
if ( ! function_exists( 'ethnic_odoo_crm_redirect_legacy_resources_page' ) ) {
function ethnic_odoo_crm_redirect_legacy_resources_page() {
if ( ! is_admin() ) {
return;
}
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
if ( ethnic_odoo_crm_legacy_resources_page_slug() !== $page ) {
return;
}
wp_safe_redirect( ethnic_odoo_crm_page_url() );
exit;
}
add_action( 'admin_init', 'ethnic_odoo_crm_redirect_legacy_resources_page', 1 );
}
if ( ! function_exists( 'ethnic_odoo_crm_add_settings_submenu' ) ) {
function ethnic_odoo_crm_add_settings_submenu() {
add_submenu_page(
'edit.php?post_type=page',
'Odoo CRM Sync',
'Odoo CRM Sync',
'manage_options',
ethnic_odoo_crm_page_slug(),
'ethnic_odoo_crm_render_settings_page'
);
}
add_action( 'admin_menu', 'ethnic_odoo_crm_add_settings_submenu', 41 );
}
if ( ! function_exists( 'ethnic_odoo_crm_sync_existing_channels' ) ) {
function ethnic_odoo_crm_sync_existing_channels( $mode ) {
$mode = sanitize_key( (string) $mode );
$results = array();
if ( in_array( $mode, array( 'resources', 'both', 'all' ), true ) && function_exists( 'ethnic_resources_page_sync_existing_submissions_to_odoo' ) ) {
$results['resource'] = ethnic_resources_page_sync_existing_submissions_to_odoo();
}
if ( in_array( $mode, array( 'proposals', 'both', 'all' ), true ) && function_exists( 'ethnic_request_proposal_page_sync_existing_submissions_to_odoo' ) ) {
$results['proposal'] = ethnic_request_proposal_page_sync_existing_submissions_to_odoo();
}
if ( in_array( $mode, array( 'contacts', 'all' ), true ) && function_exists( 'ethnic_shared_contact_form_sync_existing_submissions_to_odoo' ) ) {
$results['contact'] = ethnic_shared_contact_form_sync_existing_submissions_to_odoo();
}
return $results;
}
}
if ( ! function_exists( 'ethnic_odoo_crm_format_sync_results' ) ) {
function ethnic_odoo_crm_format_sync_results( $results ) {
if ( empty( $results ) || ! is_array( $results ) ) {
return '';
}
$messages = array();
$labels = ethnic_odoo_crm_channels();
foreach ( $results as $channel => $result ) {
$label = isset( $labels[ $channel ]['label'] ) ? $labels[ $channel ]['label'] : ucfirst( $channel );
if ( is_wp_error( $result ) ) {
$messages[] = $label . ': ' . $result->get_error_message();
continue;
}
$messages[] = sprintf(
'%1$s synced: %2$d synced, %3$d failed, %4$d already synced.',
$label,
(int) ( isset( $result['synced'] ) ? $result['synced'] : 0 ),
(int) ( isset( $result['failed'] ) ? $result['failed'] : 0 ),
(int) ( isset( $result['skipped'] ) ? $result['skipped'] : 0 )
);
}
return implode( ' ', array_filter( $messages ) );
}
}
if ( ! function_exists( 'ethnic_odoo_crm_handle_settings_save' ) ) {
function ethnic_odoo_crm_handle_settings_save() {
if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
return;
}
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
if ( ethnic_odoo_crm_page_slug() !== $page || empty( $_POST['ethnic_odoo_crm_settings_nonce'] ) ) {
return;
}
check_admin_referer( 'ethnic_odoo_crm_settings', 'ethnic_odoo_crm_settings_nonce' );
$posted = isset( $_POST['ethnic_odoo_crm'] ) && is_array( $_POST['ethnic_odoo_crm'] ) ? wp_unslash( $_POST['ethnic_odoo_crm'] ) : array();
$existing = ethnic_odoo_crm_settings();
$settings = ethnic_odoo_crm_sanitize_settings( $posted, $existing );
$health = ethnic_odoo_crm_test_connection( $settings );
$settings['last_connection_status'] = sanitize_key( $health['status'] );
$settings['last_connection_message'] = sanitize_text_field( $health['message'] );
$settings['last_checked_at'] = current_time( 'mysql' );
$settings['last_uid'] = ! empty( $health['uid'] ) ? absint( $health['uid'] ) : 0;
update_option( ETHNIC_ODOO_CRM_SETTINGS_OPTION_KEY, $settings, false );
ethnic_odoo_crm_mirror_resource_settings( $settings );
$message = 'Odoo CRM settings saved.';
$type = 'success' === $health['status'] ? 'success' : ( 'error' === $health['status'] ? 'error' : 'warning' );
if ( ! empty( $health['message'] ) ) {
$message .= ' ' . $health['message'];
}
$sync_mode = isset( $_POST['ethnic_odoo_crm_sync_existing'] ) ? sanitize_key( wp_unslash( $_POST['ethnic_odoo_crm_sync_existing'] ) ) : '';
if ( in_array( $sync_mode, array( 'resources', 'proposals', 'contacts', 'both', 'all' ), true ) ) {
$results = ethnic_odoo_crm_sync_existing_channels( $sync_mode );
$summary = ethnic_odoo_crm_format_sync_results( $results );
if ( $summary ) {
$message .= ' ' . $summary;
}
foreach ( (array) $results as $result ) {
if ( is_wp_error( $result ) || ( is_array( $result ) && ! empty( $result['failed'] ) ) ) {
if ( 'error' !== $type ) {
$type = 'warning';
}
break;
}
}
}
ethnic_odoo_crm_set_admin_notice( $type, $message );
wp_safe_redirect( ethnic_odoo_crm_page_url() );
exit;
}
add_action( 'admin_init', 'ethnic_odoo_crm_handle_settings_save' );
}
if ( ! function_exists( 'ethnic_odoo_crm_render_settings_page' ) ) {
function ethnic_odoo_crm_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to manage Odoo CRM sync settings.', 403 );
}
$settings = ethnic_odoo_crm_settings();
$channels = ethnic_odoo_crm_channels();
?>
<div class="wrap">
<h1>Odoo CRM Sync</h1>
<p>Manage one shared Odoo CRM connection for Resource Downloads, Proposal Requests, and the website contact form. Each channel can keep its own lead prefix, source label, and lead vs opportunity behavior.</p>
<div style="display:grid;gap:12px;margin-top:18px;">
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
<strong>Connection status:</strong>
<?php echo wp_kses_post( ethnic_odoo_crm_connection_badge_html( $settings['last_connection_status'] ) ); ?>
</div>
<?php if ( ! empty( $settings['last_connection_message'] ) ) : ?>
<p style="margin:0;"><?php echo esc_html( $settings['last_connection_message'] ); ?></p>
<?php endif; ?>
<div style="display:flex;flex-wrap:wrap;gap:18px;color:#5f6f8b;font-size:13px;">
<span><strong>Odoo URL:</strong> <?php echo esc_html( $settings['base_url'] ? $settings['base_url'] : 'Not set yet' ); ?></span>
<?php if ( ! empty( $settings['last_checked_at'] ) ) : ?>
<span><strong>Last checked:</strong> <?php echo esc_html( $settings['last_checked_at'] ); ?></span>
<?php endif; ?>
<?php if ( ! empty( $settings['last_uid'] ) ) : ?>
<span><strong>Authenticated user ID:</strong> <?php echo esc_html( (string) $settings['last_uid'] ); ?></span>
<?php endif; ?>
</div>
</div>
<form method="post" action="<?php echo esc_url( ethnic_odoo_crm_page_url() ); ?>" style="margin-top:20px;">
<?php wp_nonce_field( 'ethnic_odoo_crm_settings', 'ethnic_odoo_crm_settings_nonce' ); ?>
<div style="background:#fff;border:1px solid #d9e1ef;border-radius:16px;padding:24px;margin-bottom:18px;">
<h2 style="margin-top:0;">Connection</h2>
<p style="margin-top:0;color:#5f6f8b;">Use credentials for an Odoo account that can create CRM leads. Leave the password/API key blank if you want to keep the current stored secret.</p>
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 18px;">
<label style="grid-column:1 / -1;">
<span style="display:block;font-weight:600;margin-bottom:6px;">Enable Odoo CRM sync</span>
<span style="display:flex;align-items:center;gap:10px;">
<input type="checkbox" name="ethnic_odoo_crm[enabled]" value="1" <?php checked( ! empty( $settings['enabled'] ) ); ?> />
<span>Allow enabled website channels to create Odoo CRM leads.</span>
</span>
</label>
<label style="grid-column:1 / -1;">
<span style="display:block;font-weight:600;margin-bottom:6px;">Odoo base URL</span>
<input type="url" name="ethnic_odoo_crm[base_url]" value="<?php echo esc_attr( $settings['base_url'] ); ?>" placeholder="https://work.ethnicinfotech.in" style="width:100%;" />
<span style="display:block;color:#5f6f8b;margin-top:6px;">Use the base domain only. If you paste <code>/web</code>, it will be normalized automatically.</span>
</label>
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Database name</span>
<input type="text" name="ethnic_odoo_crm[database]" value="<?php echo esc_attr( $settings['database'] ); ?>" placeholder="odoo-database-name" style="width:100%;" />
</label>
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Username / email</span>
<input type="text" name="ethnic_odoo_crm[username]" value="<?php echo esc_attr( $settings['username'] ); ?>" placeholder="crm@example.com" style="width:100%;" />
</label>
<label style="grid-column:1 / -1;">
<span style="display:block;font-weight:600;margin-bottom:6px;">Password or API key</span>
<input type="password" name="ethnic_odoo_crm[password]" value="" placeholder="<?php echo esc_attr( ! empty( $settings['password'] ) ? 'Stored already — leave blank to keep the current secret' : 'Enter password or API key' ); ?>" style="width:100%;" />
</label>
</div>
</div>
<div style="background:#fff;border:1px solid #d9e1ef;border-radius:16px;padding:24px;margin-bottom:18px;">
<h2 style="margin-top:0;">CRM Routing</h2>
<p style="margin-top:0;color:#5f6f8b;">Optional routing values can assign leads to a team or user and build direct Odoo lead URLs that open inside the right CRM menu context.</p>
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 18px;">
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Sales team ID</span>
<input type="text" name="ethnic_odoo_crm[team_id]" value="<?php echo esc_attr( ! empty( $settings['team_id'] ) ? (string) $settings['team_id'] : '' ); ?>" placeholder="Optional numeric team ID" style="width:100%;" />
</label>
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Assigned user ID</span>
<input type="text" name="ethnic_odoo_crm[user_id]" value="<?php echo esc_attr( ! empty( $settings['user_id'] ) ? (string) $settings['user_id'] : '' ); ?>" placeholder="Optional numeric user ID" style="width:100%;" />
</label>
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Company IDs (`cids`)</span>
<input type="text" name="ethnic_odoo_crm[cids]" value="<?php echo esc_attr( $settings['cids'] ); ?>" placeholder="Example: 1" style="width:100%;" />
</label>
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Menu ID</span>
<input type="text" name="ethnic_odoo_crm[menu_id]" value="<?php echo esc_attr( ! empty( $settings['menu_id'] ) ? (string) $settings['menu_id'] : '' ); ?>" placeholder="Example: 277" style="width:100%;" />
</label>
<label>
<span style="display:block;font-weight:600;margin-bottom:6px;">Action ID</span>
<input type="text" name="ethnic_odoo_crm[action_id]" value="<?php echo esc_attr( ! empty( $settings['action_id'] ) ? (string) $settings['action_id'] : '' ); ?>" placeholder="Example: 429" style="width:100%;" />
</label>
</div>
</div>
<div style="background:#fff;border:1px solid #d9e1ef;border-radius:16px;padding:24px;margin-bottom:18px;">
<h2 style="margin-top:0;">Channel Mapping</h2>
<p style="margin-top:0;color:#5f6f8b;">Each website lead channel can stay on the same Odoo connection while using its own prefix, source label, and lead vs opportunity mapping.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:18px;">
<?php foreach ( $channels as $channel => $definition ) : ?>
<?php $channel_settings = ethnic_odoo_crm_channel_settings( $channel, $settings ); ?>
<div style="border:1px solid #d9e1ef;border-radius:16px;padding:18px;background:#f8fbff;">
<h3 style="margin-top:0;margin-bottom:8px;"><?php echo esc_html( $definition['label'] ); ?></h3>
<p style="margin-top:0;color:#5f6f8b;min-height:48px;"><?php echo esc_html( $definition['description'] ); ?></p>
<label style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
<input type="checkbox" name="ethnic_odoo_crm[channels][<?php echo esc_attr( $channel ); ?>][enabled]" value="1" <?php checked( ! empty( $settings['channels'][ $channel ]['enabled'] ) ); ?> />
<span>Enable this channel</span>
</label>
<label style="display:block;margin-bottom:12px;">
<span style="display:block;font-weight:600;margin-bottom:6px;">Odoo record type</span>
<select name="ethnic_odoo_crm[channels][<?php echo esc_attr( $channel ); ?>][lead_type]" style="width:100%;">
<option value="lead" <?php selected( 'lead', $channel_settings['lead_type'] ); ?>>Lead</option>
<option value="opportunity" <?php selected( 'opportunity', $channel_settings['lead_type'] ); ?>>Opportunity</option>
</select>
</label>
<label style="display:block;margin-bottom:12px;">
<span style="display:block;font-weight:600;margin-bottom:6px;">Lead title prefix</span>
<input type="text" name="ethnic_odoo_crm[channels][<?php echo esc_attr( $channel ); ?>][lead_prefix]" value="<?php echo esc_attr( $settings['channels'][ $channel ]['lead_prefix'] ); ?>" style="width:100%;" />
</label>
<label style="display:block;">
<span style="display:block;font-weight:600;margin-bottom:6px;">Source label</span>
<input type="text" name="ethnic_odoo_crm[channels][<?php echo esc_attr( $channel ); ?>][source_label]" value="<?php echo esc_attr( $settings['channels'][ $channel ]['source_label'] ); ?>" style="width:100%;" />
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<p style="display:flex;flex-wrap:wrap;gap:12px;">
<button type="submit" class="button button-primary button-large">Save Odoo CRM Settings</button>
<button type="submit" class="button button-secondary button-large" name="ethnic_odoo_crm_sync_existing" value="resources">Save and Sync Resource Submissions</button>
<button type="submit" class="button button-secondary button-large" name="ethnic_odoo_crm_sync_existing" value="proposals">Save and Sync Proposal Requests</button>
<button type="submit" class="button button-secondary button-large" name="ethnic_odoo_crm_sync_existing" value="contacts">Save and Sync Contact Submissions</button>
<button type="submit" class="button button-secondary button-large" name="ethnic_odoo_crm_sync_existing" value="all">Save and Sync All</button>
</p>
</form>
</div>
<?php
}
}