From 1f86a7c49772df434d071747049c51cce7e7a89c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 31 Mar 2026 20:48:16 -0400 Subject: [PATCH] feat: complete fusion-woodoo WordPress plugin with portal, REST endpoints, webhooks, and admin settings Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fusion-woodoo/assets/css/admin.css | 74 ++++ .../fusion-woodoo/assets/css/my-account.css | 376 ++++++++++++++++++ .../fusion-woodoo/assets/js/admin.js | 32 ++ .../fusion-woodoo/assets/js/my-account.js | 134 +++++++ .../fusion-woodoo/fusion-woodoo.php | 51 +++ .../includes/class-admin-settings.php | 138 +++++++ .../includes/class-api-client.php | 91 +++++ .../includes/class-fusion-woodoo.php | 104 +++++ .../includes/class-my-account.php | 127 ++++++ .../includes/class-order-timeline.php | 42 ++ .../includes/class-rest-endpoints.php | 189 +++++++++ .../fusion-woodoo/includes/class-returns.php | 103 +++++ .../fusion-woodoo/includes/class-webhooks.php | 106 +++++ fusion-woo-odoo/fusion-woodoo/readme.txt | 91 +++++ .../templates/admin/settings.php | 85 ++++ .../templates/my-account/communication.php | 36 ++ .../templates/my-account/deliveries.php | 105 +++++ .../templates/my-account/invoices.php | 88 ++++ .../templates/my-account/order-timeline.php | 49 +++ .../templates/my-account/returns.php | 99 +++++ .../templates/my-account/sales-orders.php | 56 +++ 21 files changed, 2176 insertions(+) create mode 100644 fusion-woo-odoo/fusion-woodoo/assets/css/admin.css create mode 100644 fusion-woo-odoo/fusion-woodoo/assets/css/my-account.css create mode 100644 fusion-woo-odoo/fusion-woodoo/assets/js/admin.js create mode 100644 fusion-woo-odoo/fusion-woodoo/assets/js/my-account.js create mode 100644 fusion-woo-odoo/fusion-woodoo/fusion-woodoo.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-admin-settings.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-api-client.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-fusion-woodoo.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-my-account.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-order-timeline.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-rest-endpoints.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-returns.php create mode 100644 fusion-woo-odoo/fusion-woodoo/includes/class-webhooks.php create mode 100644 fusion-woo-odoo/fusion-woodoo/readme.txt create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/admin/settings.php create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/my-account/communication.php create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/my-account/deliveries.php create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/my-account/invoices.php create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/my-account/order-timeline.php create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/my-account/returns.php create mode 100644 fusion-woo-odoo/fusion-woodoo/templates/my-account/sales-orders.php diff --git a/fusion-woo-odoo/fusion-woodoo/assets/css/admin.css b/fusion-woo-odoo/fusion-woodoo/assets/css/admin.css new file mode 100644 index 00000000..1e9417c9 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/assets/css/admin.css @@ -0,0 +1,74 @@ +/* Fusion WooDoo — Admin Styles */ + +.fusion-woodoo-admin h1 { + font-size: 1.5rem; + margin-bottom: 8px; +} + +.fusion-woodoo-header { + margin-bottom: 20px; + color: #555; +} + +.fusion-woodoo-card { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 20px 24px; + margin-bottom: 20px; + max-width: 900px; +} + +.fusion-woodoo-card h2 { + font-size: 1.1rem; + margin-top: 0; + margin-bottom: 16px; + padding-bottom: 10px; + border-bottom: 1px solid #f0f0f0; +} + +/* Status badges */ +.fusion-woodoo-status { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; +} + +.fusion-woodoo-status--ok { + background: #e8f5e9; + color: #2e7d32; +} + +.fusion-woodoo-status--error { + background: #fce4ec; + color: #c62828; +} + +/* Test connection result */ +.fusion-woodoo-test-result { + font-weight: 500; + font-size: 0.9rem; +} + +.fusion-woodoo-test-result.success { + color: #2e7d32; +} + +.fusion-woodoo-test-result.error { + color: #c62828; +} + +/* Webhook table */ +.fusion-woodoo-card .widefat { + border-collapse: collapse; + width: 100%; + margin-top: 12px; +} + +.fusion-woodoo-card .widefat th, +.fusion-woodoo-card .widefat td { + padding: 8px 12px; + font-size: 0.875rem; +} diff --git a/fusion-woo-odoo/fusion-woodoo/assets/css/my-account.css b/fusion-woo-odoo/fusion-woodoo/assets/css/my-account.css new file mode 100644 index 00000000..e5f65bd0 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/assets/css/my-account.css @@ -0,0 +1,376 @@ +/* Fusion WooDoo — My Account / Frontend Styles */ + +/* ── Portal wrapper ─────────────────────────────────────────── */ +.fusion-woodoo-portal { + font-size: 0.95rem; +} + +.fusion-woodoo-portal h2 { + font-size: 1.3rem; + margin-bottom: 16px; +} + +.fusion-woodoo-portal h3 { + font-size: 1.1rem; + margin: 24px 0 12px; +} + +/* ── Tables ─────────────────────────────────────────────────── */ +.fusion-woodoo-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 24px; +} + +.fusion-woodoo-table th, +.fusion-woodoo-table td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid #f0f0f0; + vertical-align: middle; +} + +.fusion-woodoo-table thead th { + background: #f8f9fa; + font-weight: 600; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #555; +} + +.fusion-woodoo-table tbody tr:hover { + background: #fafafa; +} + +/* ── Status badges ──────────────────────────────────────────── */ +.fusion-woodoo-badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.78rem; + font-weight: 600; + background: #e9ecef; + color: #495057; +} + +.fusion-woodoo-badge--sale, +.fusion-woodoo-badge--done, +.fusion-woodoo-badge--posted, +.fusion-woodoo-badge--paid { + background: #e8f5e9; + color: #2e7d32; +} + +.fusion-woodoo-badge--draft, +.fusion-woodoo-badge--pending { + background: #fff8e1; + color: #f57f17; +} + +.fusion-woodoo-badge--cancel { + background: #fce4ec; + color: #c62828; +} + +.fusion-woodoo-badge--processing { + background: #e3f2fd; + color: #1565c0; +} + +/* ── Utility ────────────────────────────────────────────────── */ +.fusion-woodoo-empty { + color: #888; + font-style: italic; + padding: 16px 0; +} + +.fusion-woodoo-muted { + color: #aaa; +} + +.fusion-woodoo-notice { + padding: 12px 16px; + border-radius: 4px; + margin-bottom: 16px; + font-size: 0.9rem; +} + +.fusion-woodoo-notice--success { + background: #e8f5e9; + color: #2e7d32; + border-left: 4px solid #43a047; +} + +.fusion-woodoo-notice--error { + background: #fce4ec; + color: #c62828; + border-left: 4px solid #e53935; +} + +.fusion-woodoo-notice--warning { + background: #fff8e1; + color: #f57f17; + border-left: 4px solid #ffb300; +} + +/* ── PDF button ─────────────────────────────────────────────── */ +.fusion-woodoo-btn-pdf { + font-size: 0.82rem !important; + padding: 4px 10px !important; +} + +/* ── Forms ──────────────────────────────────────────────────── */ +.fusion-woodoo-form { + max-width: 580px; +} + +.fusion-woodoo-form-row { + margin-bottom: 18px; +} + +.fusion-woodoo-form-row label { + display: block; + font-weight: 600; + margin-bottom: 6px; + font-size: 0.9rem; +} + +.fusion-woodoo-form-row select, +.fusion-woodoo-form-row textarea { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + font-family: inherit; + background: #fff; +} + +.fusion-woodoo-form-row textarea { + resize: vertical; +} + +/* Item checkboxes in return form */ +.fw-return-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid #f5f5f5; +} + +.fw-return-item:last-child { + border-bottom: none; +} + +.fw-return-item input[type="checkbox"] { + flex-shrink: 0; +} + +.fw-return-item-qty { + width: 60px; + padding: 4px 8px; + border: 1px solid #ddd; + border-radius: 4px; + text-align: center; +} + +/* ── Order Timeline ─────────────────────────────────────────── */ +.fusion-woodoo-timeline-wrap { + margin: 28px 0 12px; + padding: 20px 0; + border-top: 1px solid #f0f0f0; +} + +.fusion-woodoo-timeline-wrap h3 { + font-size: 1rem; + margin-bottom: 20px; + color: #333; +} + +.fusion-woodoo-timeline { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0; +} + +.fusion-woodoo-timeline__step { + display: flex; + flex-direction: column; + align-items: center; + min-width: 80px; +} + +.fusion-woodoo-timeline__dot { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid #ccc; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + position: relative; + transition: all 0.2s ease; +} + +.fusion-woodoo-timeline__step--done .fusion-woodoo-timeline__dot { + background: #43a047; + border-color: #43a047; + color: #fff; +} + +.fusion-woodoo-timeline__step--done .fusion-woodoo-timeline__dot svg { + width: 14px; + height: 14px; +} + +.fusion-woodoo-timeline__step--active .fusion-woodoo-timeline__dot { + background: #1976d2; + border-color: #1976d2; + box-shadow: 0 0 0 4px rgba(25, 118, 210, 0.15); +} + +.fusion-woodoo-timeline__step--pending .fusion-woodoo-timeline__dot { + background: #f5f5f5; + border-color: #ddd; +} + +.fusion-woodoo-timeline__label { + margin-top: 8px; + font-size: 0.78rem; + text-align: center; + color: #555; + max-width: 80px; +} + +.fusion-woodoo-timeline__step--active .fusion-woodoo-timeline__label { + color: #1976d2; + font-weight: 600; +} + +.fusion-woodoo-timeline__step--done .fusion-woodoo-timeline__label { + color: #2e7d32; +} + +.fusion-woodoo-timeline__tracking { + display: block; + font-size: 0.75rem; + color: #777; + margin-top: 4px; + max-width: 100px; + word-break: break-all; +} + +.fusion-woodoo-timeline__connector { + flex: 1; + height: 2px; + background: #ddd; + margin-bottom: 28px; + min-width: 20px; +} + +.fusion-woodoo-timeline__connector--done { + background: #43a047; +} + +/* ── Communication / Messages ───────────────────────────────── */ +.fusion-woodoo-communication { + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid #f0f0f0; +} + +.fusion-woodoo-messages { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fusion-woodoo-message { + border: 1px solid #ebebeb; + border-radius: 6px; + padding: 12px 16px; + background: #fafafa; +} + +.fusion-woodoo-message--email { + border-left: 4px solid #1976d2; +} + +.fusion-woodoo-message--note { + border-left: 4px solid #f9a825; + background: #fffde7; +} + +.fusion-woodoo-message__meta { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 6px; + font-size: 0.82rem; + color: #666; +} + +.fusion-woodoo-message__author { + font-weight: 600; + color: #333; +} + +.fusion-woodoo-message__type-badge { + padding: 1px 7px; + border-radius: 10px; + background: #e3f2fd; + color: #1565c0; + font-size: 0.75rem; + font-weight: 500; +} + +.fusion-woodoo-message__type-badge--note { + background: #fff8e1; + color: #f57f17; +} + +.fusion-woodoo-message__body { + font-size: 0.9rem; + line-height: 1.6; + color: #444; +} + +/* ── Responsive ─────────────────────────────────────────────── */ +@media (max-width: 640px) { + .fusion-woodoo-table th, + .fusion-woodoo-table td { + padding: 8px 8px; + font-size: 0.85rem; + } + + .fusion-woodoo-timeline { + flex-direction: column; + align-items: flex-start; + gap: 0; + } + + .fusion-woodoo-timeline__step { + flex-direction: row; + align-items: flex-start; + gap: 12px; + min-width: unset; + } + + .fusion-woodoo-timeline__connector { + width: 2px; + height: 24px; + flex: none; + margin: 0 13px 0; + min-width: unset; + } + + .fusion-woodoo-timeline__label { + text-align: left; + max-width: unset; + margin-top: 4px; + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/assets/js/admin.js b/fusion-woo-odoo/fusion-woodoo/assets/js/admin.js new file mode 100644 index 00000000..529df043 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/assets/js/admin.js @@ -0,0 +1,32 @@ +/* Fusion WooDoo — Admin JS */ +(function ($) { + 'use strict'; + + $(function () { + const $btn = $('#fusion-woodoo-test-connection'); + const $result = $('#fusion-woodoo-test-result'); + + $btn.on('click', function () { + $btn.prop('disabled', true).text('Testing…'); + $result.hide().removeClass('success error'); + + $.post(fusionWooDooAdmin.ajaxUrl, { + action: 'fusion_woodoo_test_connection', + nonce: fusionWooDooAdmin.nonce, + }) + .done(function (res) { + if (res.success) { + $result.addClass('success').text(res.data.message).show(); + } else { + $result.addClass('error').text(res.data.message || 'Connection failed.').show(); + } + }) + .fail(function () { + $result.addClass('error').text('Request failed. Check your network connection.').show(); + }) + .always(function () { + $btn.prop('disabled', false).text('Test Connection'); + }); + }); + }); +}(jQuery)); diff --git a/fusion-woo-odoo/fusion-woodoo/assets/js/my-account.js b/fusion-woo-odoo/fusion-woodoo/assets/js/my-account.js new file mode 100644 index 00000000..d19166f0 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/assets/js/my-account.js @@ -0,0 +1,134 @@ +/* Fusion WooDoo — My Account JS */ +(function ($) { + 'use strict'; + + /* ── Return form ──────────────────────────────────────────── */ + var ordersData = {}; + try { + var raw = document.getElementById('fw-orders-data'); + if (raw) ordersData = JSON.parse(raw.textContent || '{}'); + } catch (e) {} + + // Populate item list when order changes + $(document).on('change', '#fw-order-select', function () { + var orderId = $(this).val(); + var $container = $('#fw-items-container'); + var $list = $('#fw-items-list'); + $list.empty(); + + if (!orderId || !ordersData[orderId]) { + $container.hide(); + return; + } + + var items = ordersData[orderId]; + if (!items.length) { + $container.hide(); + return; + } + + items.forEach(function (item) { + var $row = $('
'); + $row.append( + $('').attr({ + name: 'items[' + item.product_id + '][select]', + 'data-product-id': item.product_id, + value: '1', + }) + ); + $row.append($('').text(item.name)); + $row.append( + $('').attr({ + name: 'items[' + item.product_id + '][qty]', + min: 1, + max: item.qty, + value: item.qty, + disabled: true, + }) + ); + $list.append($row); + }); + + $container.show(); + }); + + // Enable/disable qty field with checkbox + $(document).on('change', '.fw-return-item input[type="checkbox"]', function () { + var $qty = $(this).closest('.fw-return-item').find('.fw-return-item-qty'); + $qty.prop('disabled', !this.checked); + }); + + // Submit return form via AJAX + $(document).on('submit', '#fusion-woodoo-return-form', function (e) { + e.preventDefault(); + + var $form = $(this); + var $notice = $('#fusion-woodoo-return-notice'); + var $submit = $form.find('[type=submit]'); + + // Collect selected items + var items = []; + $form.find('.fw-return-item input[type="checkbox"]:checked').each(function () { + var productId = $(this).data('product-id'); + var qty = parseInt($(this).closest('.fw-return-item').find('.fw-return-item-qty').val(), 10) || 1; + items.push({ product_id: productId, qty: qty }); + }); + + var payload = { + action: 'fusion_woodoo_submit_return', + nonce: fusionWooDoo.nonce, + order_id: $form.find('[name=order_id]').val(), + reason: $form.find('[name=reason]').val(), + items: items, + }; + + $submit.prop('disabled', true).text('Submitting…'); + $notice.hide().removeClass('fusion-woodoo-notice--success fusion-woodoo-notice--error'); + + $.post(fusionWooDoo.ajaxUrl, payload) + .done(function (res) { + if (res.success) { + $notice.addClass('fusion-woodoo-notice--success').text(res.data.message).show(); + $form[0].reset(); + $('#fw-items-container').hide(); + } else { + $notice.addClass('fusion-woodoo-notice--error').text(res.data.message || 'Submission failed.').show(); + } + }) + .fail(function () { + $notice.addClass('fusion-woodoo-notice--error').text('Request failed. Please try again.').show(); + }) + .always(function () { + $submit.prop('disabled', false).text('Submit Return Request'); + $('html, body').animate({ scrollTop: $notice.offset().top - 40 }, 300); + }); + }); + + /* ── Reorder button ───────────────────────────────────────── */ + $(document).on('click', '.fusion-woodoo-reorder', function () { + var $btn = $(this); + var orderId = $btn.data('order-id'); + var nonce = $btn.data('nonce'); + + $btn.prop('disabled', true).text('Adding…'); + + $.post(fusionWooDoo.ajaxUrl, { + action: 'fusion_woodoo_reorder', + nonce: nonce, + order_id: orderId, + }) + .done(function (res) { + if (res.success && res.data.cart_url) { + window.location.href = res.data.cart_url; + } else { + alert(res.data.message || 'Could not reorder. Please try again.'); + $btn.prop('disabled', false).text('Reorder'); + } + }) + .fail(function () { + alert('Request failed. Please try again.'); + $btn.prop('disabled', false).text('Reorder'); + }); + }); + +}(jQuery)); diff --git a/fusion-woo-odoo/fusion-woodoo/fusion-woodoo.php b/fusion-woo-odoo/fusion-woodoo/fusion-woodoo.php new file mode 100644 index 00000000..9ee03cb0 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/fusion-woodoo.php @@ -0,0 +1,51 @@ +

Fusion WooDoo requires WooCommerce to be installed and active.

'; + }); + return; + } + require_once FUSION_WOODOO_PLUGIN_DIR . 'includes/class-fusion-woodoo.php'; + Fusion_WooDoo::instance(); +}); + +register_activation_hook(__FILE__, function() { + $dirs = ['invoices', 'deliveries']; + foreach ($dirs as $dir) { + $path = wp_upload_dir()['basedir'] . '/fusion-woodoo/' . $dir; + wp_mkdir_p($path); + file_put_contents($path . '/.htaccess', 'deny from all'); + } + flush_rewrite_rules(); +}); + +register_deactivation_hook(__FILE__, function() { + if (file_exists(FUSION_WOODOO_PLUGIN_DIR . 'includes/class-webhooks.php')) { + require_once FUSION_WOODOO_PLUGIN_DIR . 'includes/class-webhooks.php'; + Fusion_WooDoo_Webhooks::unregister_all(); + } + flush_rewrite_rules(); +}); diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-admin-settings.php b/fusion-woo-odoo/fusion-woodoo/includes/class-admin-settings.php new file mode 100644 index 00000000..f37e2e2b --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-admin-settings.php @@ -0,0 +1,138 @@ + 'sanitize_text_field']); + } + + add_settings_section( + 'fusion_woodoo_connection', + __('Odoo Connection', 'fusion-woodoo'), + null, + 'fusion-woodoo-settings' + ); + + add_settings_field( + self::OPTION_ODOO_URL, + __('Odoo URL', 'fusion-woodoo'), + [$this, 'field_odoo_url'], + 'fusion-woodoo-settings', + 'fusion_woodoo_connection' + ); + + add_settings_field( + self::OPTION_API_KEY, + __('API Key', 'fusion-woodoo'), + [$this, 'field_api_key'], + 'fusion-woodoo-settings', + 'fusion_woodoo_connection' + ); + + add_settings_section( + 'fusion_woodoo_portal', + __('Customer Portal Tabs', 'fusion-woodoo'), + null, + 'fusion-woodoo-settings' + ); + + $toggles = [ + self::OPTION_SHOW_ORDERS => __('Show Sales Orders tab', 'fusion-woodoo'), + self::OPTION_SHOW_INVOICES => __('Show Invoices tab', 'fusion-woodoo'), + self::OPTION_SHOW_DELIVERIES => __('Show Deliveries tab', 'fusion-woodoo'), + self::OPTION_SHOW_RETURNS => __('Show Returns tab', 'fusion-woodoo'), + ]; + foreach ($toggles as $option => $label) { + add_settings_field( + $option, + $label, + [$this, 'field_checkbox'], + 'fusion-woodoo-settings', + 'fusion_woodoo_portal', + ['option' => $option] + ); + } + } + + public function field_odoo_url(): void { + $value = esc_attr(get_option(self::OPTION_ODOO_URL, '')); + echo ''; + echo '

' . __('Base URL of your Odoo instance — no trailing slash.', 'fusion-woodoo') . '

'; + } + + public function field_api_key(): void { + $value = esc_attr(get_option(self::OPTION_API_KEY, '')); + echo ''; + echo '

' . __('API key from the Fusion WooCommerce Odoo module.', 'fusion-woodoo') . '

'; + } + + public function field_checkbox(array $args): void { + $option = $args['option']; + $checked = checked(1, get_option($option, 1), false); + echo ''; + } + + public function render_settings_page(): void { + $template = FUSION_WOODOO_PLUGIN_DIR . 'templates/admin/settings.php'; + if (file_exists($template)) { + include $template; + } + } + + public function on_url_changed(): void { + if (class_exists('Fusion_WooDoo_Webhooks')) { + Fusion_WooDoo_Webhooks::unregister_all(); + (new Fusion_WooDoo_Webhooks())->register_webhooks(); + } + } + + public function ajax_test_connection(): void { + check_ajax_referer('fusion_woodoo_admin_nonce', 'nonce'); + if (!current_user_can('manage_woocommerce')) { + wp_send_json_error(['message' => __('Permission denied.', 'fusion-woodoo')]); + } + $client = new Fusion_WooDoo_API_Client(); + $result = $client->test_connection(); + if ($result['success']) { + wp_send_json_success(['message' => __('Connection successful! Odoo is reachable.', 'fusion-woodoo')]); + } else { + wp_send_json_error(['message' => $result['error'] ?: __('Connection failed.', 'fusion-woodoo')]); + } + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-api-client.php b/fusion-woo-odoo/fusion-woodoo/includes/class-api-client.php new file mode 100644 index 00000000..d2b461ed --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-api-client.php @@ -0,0 +1,91 @@ +odoo_url = rtrim(get_option('fusion_woodoo_odoo_url', ''), '/'); + $this->api_key = get_option('fusion_woodoo_api_key', ''); + } + + /** + * Make a POST request to an Odoo endpoint. + * + * @param string $endpoint e.g. '/woo/api/order/status' + * @param array $data + * @return array{success: bool, data: mixed, error: string} + */ + public function request(string $endpoint, array $data = []): array { + if (empty($this->odoo_url) || empty($this->api_key)) { + return ['success' => false, 'data' => null, 'error' => 'Odoo URL or API key not configured.']; + } + + $url = $this->odoo_url . $endpoint; + $response = wp_remote_post($url, [ + 'timeout' => 15, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $this->api_key, + ], + 'body' => wp_json_encode($data), + 'data_format' => 'body', + ]); + + if (is_wp_error($response)) { + return ['success' => false, 'data' => null, 'error' => $response->get_error_message()]; + } + + $code = wp_remote_retrieve_response_code($response); + $body = json_decode(wp_remote_retrieve_body($response), true); + + if ($code >= 200 && $code < 300) { + return ['success' => true, 'data' => $body, 'error' => '']; + } + + $error_msg = $body['message'] ?? $body['error'] ?? 'Unexpected response from Odoo (HTTP ' . $code . ').'; + return ['success' => false, 'data' => $body, 'error' => $error_msg]; + } + + /** + * Test connectivity to Odoo. + */ + public function test_connection(): array { + return $this->request('/woo/api/order/status', ['ping' => true]); + } + + /** + * Get order status from Odoo. + * + * @param int|string $order_id WooCommerce order ID + */ + public function get_order_status(int|string $order_id): array { + return $this->request('/woo/api/order/status', ['order_id' => $order_id]); + } + + /** + * Submit a return request to Odoo. + * + * @param int|string $order_id + * @param array $items [['product_id' => ..., 'qty' => ...], ...] + * @param string $reason + */ + public function submit_return(int|string $order_id, array $items, string $reason): array { + return $this->request('/woo/api/return/create', [ + 'order_id' => $order_id, + 'items' => $items, + 'reason' => $reason, + ]); + } + + /** + * Fetch existing returns for a customer. + * + * @param int|string $customer_id WooCommerce user ID + */ + public function get_returns(int|string $customer_id): array { + return $this->request('/woo/api/return/list', ['customer_id' => $customer_id]); + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-fusion-woodoo.php b/fusion-woo-odoo/fusion-woodoo/includes/class-fusion-woodoo.php new file mode 100644 index 00000000..4d68babe --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-fusion-woodoo.php @@ -0,0 +1,104 @@ +load_includes(); + $this->init_hooks(); + } + + private function load_includes(): void { + $includes = [ + 'class-api-client.php', + 'class-admin-settings.php', + 'class-rest-endpoints.php', + 'class-webhooks.php', + 'class-my-account.php', + 'class-order-timeline.php', + 'class-returns.php', + ]; + foreach ($includes as $file) { + $path = FUSION_WOODOO_PLUGIN_DIR . 'includes/' . $file; + if (file_exists($path)) { + require_once $path; + } + } + } + + private function init_hooks(): void { + add_action('init', [$this, 'load_textdomain']); + add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']); + add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); + + // Instantiate components + new Fusion_WooDoo_Admin_Settings(); + new Fusion_WooDoo_REST_Endpoints(); + new Fusion_WooDoo_Webhooks(); + new Fusion_WooDoo_My_Account(); + new Fusion_WooDoo_Order_Timeline(); + new Fusion_WooDoo_Returns(); + } + + public function load_textdomain(): void { + load_plugin_textdomain( + 'fusion-woodoo', + false, + dirname(plugin_basename(FUSION_WOODOO_PLUGIN_DIR . 'fusion-woodoo.php')) . '/languages' + ); + } + + public function enqueue_frontend_assets(): void { + if (is_account_page() || is_woocommerce()) { + wp_enqueue_style( + 'fusion-woodoo-my-account', + FUSION_WOODOO_PLUGIN_URL . 'assets/css/my-account.css', + [], + FUSION_WOODOO_VERSION + ); + wp_enqueue_script( + 'fusion-woodoo-my-account', + FUSION_WOODOO_PLUGIN_URL . 'assets/js/my-account.js', + ['jquery'], + FUSION_WOODOO_VERSION, + true + ); + wp_localize_script('fusion-woodoo-my-account', 'fusionWooDoo', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('fusion_woodoo_nonce'), + ]); + } + } + + public function enqueue_admin_assets(string $hook): void { + if (strpos($hook, 'fusion-woodoo') === false) { + return; + } + wp_enqueue_style( + 'fusion-woodoo-admin', + FUSION_WOODOO_PLUGIN_URL . 'assets/css/admin.css', + [], + FUSION_WOODOO_VERSION + ); + wp_enqueue_script( + 'fusion-woodoo-admin', + FUSION_WOODOO_PLUGIN_URL . 'assets/js/admin.js', + ['jquery'], + FUSION_WOODOO_VERSION, + true + ); + wp_localize_script('fusion-woodoo-admin', 'fusionWooDooAdmin', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('fusion_woodoo_admin_nonce'), + ]); + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-my-account.php b/fusion-woo-odoo/fusion-woodoo/includes/class-my-account.php new file mode 100644 index 00000000..c2998911 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-my-account.php @@ -0,0 +1,127 @@ + 'Sales Orders', + 'odoo-invoices' => 'Invoices', + 'odoo-deliveries' => 'Deliveries', + 'odoo-returns' => 'Returns', + ]; + + const OPTION_MAP = [ + 'odoo-sales-orders' => 'fusion_woodoo_show_orders', + 'odoo-invoices' => 'fusion_woodoo_show_invoices', + 'odoo-deliveries' => 'fusion_woodoo_show_deliveries', + 'odoo-returns' => 'fusion_woodoo_show_returns', + ]; + + const TEMPLATE_MAP = [ + 'odoo-sales-orders' => 'sales-orders.php', + 'odoo-invoices' => 'invoices.php', + 'odoo-deliveries' => 'deliveries.php', + 'odoo-returns' => 'returns.php', + ]; + + public function __construct() { + add_action('init', [$this, 'add_rewrite_endpoints']); + add_filter('woocommerce_account_menu_items', [$this, 'add_menu_items']); + foreach (array_keys(self::ENDPOINTS) as $endpoint) { + add_action('woocommerce_account_' . $endpoint . '_endpoint', [$this, 'render_endpoint_content']); + } + // AJAX handler for PDF downloads + add_action('wp_ajax_fusion_woodoo_download_pdf', [$this, 'ajax_download_pdf']); + } + + public function add_rewrite_endpoints(): void { + foreach (array_keys(self::ENDPOINTS) as $endpoint) { + add_rewrite_endpoint($endpoint, EP_ROOT | EP_PAGES); + } + } + + public function add_menu_items(array $items): array { + $new_items = []; + foreach ($items as $key => $label) { + $new_items[$key] = $label; + // Insert Odoo tabs after 'orders' + if ($key === 'orders') { + foreach (self::ENDPOINTS as $endpoint => $title) { + if (get_option(self::OPTION_MAP[$endpoint], 1)) { + $new_items[$endpoint] = __($title, 'fusion-woodoo'); + } + } + } + } + return $new_items; + } + + /** + * Generic renderer — dispatches to the correct template. + */ + public function render_endpoint_content(): void { + // Determine which endpoint triggered this action + $current = null; + foreach (array_keys(self::ENDPOINTS) as $endpoint) { + if (did_action('woocommerce_account_' . $endpoint . '_endpoint')) { + $current = $endpoint; + break; + } + } + + if (!$current || !isset(self::TEMPLATE_MAP[$current])) { + return; + } + + $template = FUSION_WOODOO_PLUGIN_DIR . 'templates/my-account/' . self::TEMPLATE_MAP[$current]; + if (file_exists($template)) { + include $template; + } + } + + /** + * AJAX: serve a stored PDF file to the logged-in customer who owns the order. + */ + public function ajax_download_pdf(): void { + check_ajax_referer('fusion_woodoo_nonce', 'nonce'); + + $order_id = (int) ($_GET['order_id'] ?? 0); + $type = sanitize_key($_GET['type'] ?? 'invoice'); + + if (!$order_id || !is_user_logged_in()) { + wp_die(__('Access denied.', 'fusion-woodoo'), 403); + } + + $order = wc_get_order($order_id); + if (!$order || $order->get_customer_id() !== get_current_user_id()) { + wp_die(__('Access denied.', 'fusion-woodoo'), 403); + } + + $meta_key = $type === 'delivery' ? '_odoo_delivery_pdf' : '_odoo_invoice_pdf'; + $rel_path = $order->get_meta($meta_key); + + if (empty($rel_path)) { + wp_die(__('PDF not found.', 'fusion-woodoo'), 404); + } + + $upload = wp_upload_dir(); + $file_path = $upload['basedir'] . $rel_path; + + if (!file_exists($file_path) || !is_file($file_path)) { + wp_die(__('PDF file not found on server.', 'fusion-woodoo'), 404); + } + + // Security: ensure path stays within uploads dir + $real_base = realpath($upload['basedir']); + $real_file = realpath($file_path); + if (!$real_file || strpos($real_file, $real_base) !== 0) { + wp_die(__('Access denied.', 'fusion-woodoo'), 403); + } + + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="' . basename($real_file) . '"'); + header('Content-Length: ' . filesize($real_file)); + readfile($real_file); + exit; + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-order-timeline.php b/fusion-woo-odoo/fusion-woodoo/includes/class-order-timeline.php new file mode 100644 index 00000000..4518fe1c --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-order-timeline.php @@ -0,0 +1,42 @@ + 'Confirmed', + 'processing' => 'Processing', + 'shipped' => 'Shipped', + 'delivered' => 'Delivered', + 'done' => 'Completed', + ]; + + public function __construct() { + add_action('woocommerce_order_details_after_order_table', [$this, 'render_timeline']); + } + + public function render_timeline(WC_Order $order): void { + $odoo_status = strtolower((string) $order->get_meta('_odoo_order_status')); + + if (empty($odoo_status)) { + return; + } + + $tracking_number = $order->get_meta('_odoo_tracking_number'); + $shipping_carrier = $order->get_meta('_odoo_shipping_carrier'); + + $stage_keys = array_keys(self::STAGES); + $current_index = array_search($odoo_status, $stage_keys); + if ($current_index === false) { + $current_index = 0; + } + + $template = FUSION_WOODOO_PLUGIN_DIR . 'templates/my-account/order-timeline.php'; + if (file_exists($template)) { + include $template; + } + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-rest-endpoints.php b/fusion-woo-odoo/fusion-woodoo/includes/class-rest-endpoints.php new file mode 100644 index 00000000..c528da05 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-rest-endpoints.php @@ -0,0 +1,189 @@ + WP_REST_Server::CREATABLE, + 'callback' => [$this, 'handle_order_update'], + 'permission_callback' => [$this, 'verify_api_key'], + 'args' => [ + 'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'], + 'status' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'], + 'tracking_number' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'], + 'shipping_carrier' => ['required' => false, 'sanitize_callback' => 'sanitize_text_field'], + ], + ]); + + register_rest_route($namespace, '/order/invoice', [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [$this, 'handle_order_invoice'], + 'permission_callback' => [$this, 'verify_api_key'], + 'args' => [ + 'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'], + 'pdf_data' => ['required' => true], + 'filename' => ['required' => false, 'sanitize_callback' => 'sanitize_file_name'], + ], + ]); + + register_rest_route($namespace, '/order/delivery', [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [$this, 'handle_order_delivery'], + 'permission_callback' => [$this, 'verify_api_key'], + 'args' => [ + 'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'], + 'pdf_data' => ['required' => true], + 'filename' => ['required' => false, 'sanitize_callback' => 'sanitize_file_name'], + ], + ]); + + register_rest_route($namespace, '/order/messages', [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [$this, 'handle_order_messages'], + 'permission_callback' => [$this, 'verify_api_key'], + 'args' => [ + 'order_id' => ['required' => true, 'validate_callback' => 'is_numeric'], + 'messages' => ['required' => true], + ], + ]); + } + + /** + * Validate Bearer token from Authorization header. + */ + public function verify_api_key(WP_REST_Request $request): bool|WP_Error { + $auth_header = $request->get_header('authorization'); + if (empty($auth_header) || !str_starts_with($auth_header, 'Bearer ')) { + return new WP_Error('missing_auth', __('Authorization header missing or malformed.', 'fusion-woodoo'), ['status' => 401]); + } + + $provided_key = trim(substr($auth_header, 7)); + $stored_key = get_option('fusion_woodoo_api_key', ''); + + if (empty($stored_key) || !hash_equals($stored_key, $provided_key)) { + return new WP_Error('invalid_api_key', __('Invalid API key.', 'fusion-woodoo'), ['status' => 403]); + } + + return true; + } + + /** + * POST /order/update — update order status and tracking info. + */ + public function handle_order_update(WP_REST_Request $request): WP_REST_Response|WP_Error { + $order_id = (int) $request->get_param('order_id'); + $order = wc_get_order($order_id); + + if (!$order) { + return new WP_Error('order_not_found', __('Order not found.', 'fusion-woodoo'), ['status' => 404]); + } + + if ($status = $request->get_param('status')) { + $order->update_meta_data('_odoo_order_status', sanitize_text_field($status)); + } + if ($tracking = $request->get_param('tracking_number')) { + $order->update_meta_data('_odoo_tracking_number', sanitize_text_field($tracking)); + } + if ($carrier = $request->get_param('shipping_carrier')) { + $order->update_meta_data('_odoo_shipping_carrier', sanitize_text_field($carrier)); + } + + $order->save(); + + return rest_ensure_response(['success' => true, 'order_id' => $order_id]); + } + + /** + * POST /order/invoice — store base64-encoded invoice PDF. + */ + public function handle_order_invoice(WP_REST_Request $request): WP_REST_Response|WP_Error { + return $this->save_pdf($request, 'invoices', '_odoo_invoice_pdf'); + } + + /** + * POST /order/delivery — store base64-encoded delivery PDF. + */ + public function handle_order_delivery(WP_REST_Request $request): WP_REST_Response|WP_Error { + return $this->save_pdf($request, 'deliveries', '_odoo_delivery_pdf'); + } + + /** + * POST /order/messages — store Odoo messages array against the order. + */ + public function handle_order_messages(WP_REST_Request $request): WP_REST_Response|WP_Error { + $order_id = (int) $request->get_param('order_id'); + $order = wc_get_order($order_id); + + if (!$order) { + return new WP_Error('order_not_found', __('Order not found.', 'fusion-woodoo'), ['status' => 404]); + } + + $messages = $request->get_param('messages'); + if (!is_array($messages)) { + return new WP_Error('invalid_messages', __('Messages must be a JSON array.', 'fusion-woodoo'), ['status' => 400]); + } + + // Sanitize each message entry + $sanitized = array_map(function($msg) { + return [ + 'author' => sanitize_text_field($msg['author'] ?? ''), + 'date' => sanitize_text_field($msg['date'] ?? ''), + 'body' => wp_kses_post($msg['body'] ?? ''), + 'type' => sanitize_text_field($msg['type'] ?? 'note'), + ]; + }, $messages); + + $order->update_meta_data('_odoo_messages', $sanitized); + $order->save(); + + return rest_ensure_response(['success' => true, 'count' => count($sanitized)]); + } + + /** + * Shared logic to decode and store a PDF file. + */ + private function save_pdf(WP_REST_Request $request, string $folder, string $meta_key): WP_REST_Response|WP_Error { + $order_id = (int) $request->get_param('order_id'); + $order = wc_get_order($order_id); + + if (!$order) { + return new WP_Error('order_not_found', __('Order not found.', 'fusion-woodoo'), ['status' => 404]); + } + + $pdf_data = $request->get_param('pdf_data'); + $decoded = base64_decode($pdf_data, true); + if ($decoded === false) { + return new WP_Error('invalid_pdf', __('Invalid base64-encoded PDF data.', 'fusion-woodoo'), ['status' => 400]); + } + + $filename = $request->get_param('filename') ?: ($folder . '-' . $order_id . '-' . time() . '.pdf'); + $filename = sanitize_file_name($filename); + $upload = wp_upload_dir(); + $save_dir = $upload['basedir'] . '/fusion-woodoo/' . $folder . '/'; + + if (!file_exists($save_dir)) { + wp_mkdir_p($save_dir); + file_put_contents($save_dir . '.htaccess', 'deny from all'); + } + + $file_path = $save_dir . $filename; + $bytes = file_put_contents($file_path, $decoded); + + if ($bytes === false) { + return new WP_Error('file_write_error', __('Could not save PDF to disk.', 'fusion-woodoo'), ['status' => 500]); + } + + $relative = str_replace($upload['basedir'], '', $file_path); + $order->update_meta_data($meta_key, $relative); + $order->save(); + + return rest_ensure_response(['success' => true, 'path' => $relative]); + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-returns.php b/fusion-woo-odoo/fusion-woodoo/includes/class-returns.php new file mode 100644 index 00000000..db77817e --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-returns.php @@ -0,0 +1,103 @@ + get_current_user_id(), + 'limit' => $limit, + 'status' => ['completed', 'processing', 'on-hold'], + 'orderby' => 'date', + 'order' => 'DESC', + ]); + } + + /** + * Fetch existing returns for the current customer from Odoo. + * + * @return array + */ + public function get_existing_returns(): array { + if (!is_user_logged_in()) { + return []; + } + $client = new Fusion_WooDoo_API_Client(); + $result = $client->get_returns(get_current_user_id()); + return $result['success'] ? ($result['data']['returns'] ?? []) : []; + } + + /** + * AJAX handler for return form submission. + */ + public function ajax_submit_return(): void { + check_ajax_referer('fusion_woodoo_nonce', 'nonce'); + + if (!is_user_logged_in()) { + wp_send_json_error(['message' => __('You must be logged in to submit a return.', 'fusion-woodoo')]); + } + + $order_id = (int) ($_POST['order_id'] ?? 0); + $reason = sanitize_textarea_field($_POST['reason'] ?? ''); + $items = $_POST['items'] ?? []; + + if (!$order_id) { + wp_send_json_error(['message' => __('Please select an order.', 'fusion-woodoo')]); + } + + if (empty($items) || !is_array($items)) { + wp_send_json_error(['message' => __('Please select at least one item to return.', 'fusion-woodoo')]); + } + + if (empty($reason)) { + wp_send_json_error(['message' => __('Please provide a reason for the return.', 'fusion-woodoo')]); + } + + // Verify the order belongs to the current user + $order = wc_get_order($order_id); + if (!$order || $order->get_customer_id() !== get_current_user_id()) { + wp_send_json_error(['message' => __('Order not found or access denied.', 'fusion-woodoo')]); + } + + // Sanitize and validate items + $sanitized_items = []; + foreach ($items as $item) { + $product_id = (int) ($item['product_id'] ?? 0); + $qty = max(1, (int) ($item['qty'] ?? 1)); + if ($product_id > 0) { + $sanitized_items[] = ['product_id' => $product_id, 'qty' => $qty]; + } + } + + if (empty($sanitized_items)) { + wp_send_json_error(['message' => __('No valid items selected for return.', 'fusion-woodoo')]); + } + + $client = new Fusion_WooDoo_API_Client(); + $result = $client->submit_return($order_id, $sanitized_items, $reason); + + if ($result['success']) { + wp_send_json_success([ + 'message' => __('Return request submitted successfully. Our team will review it shortly.', 'fusion-woodoo'), + 'return_id' => $result['data']['return_id'] ?? null, + ]); + } else { + wp_send_json_error([ + 'message' => $result['error'] ?: __('Failed to submit return. Please try again or contact support.', 'fusion-woodoo'), + ]); + } + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/includes/class-webhooks.php b/fusion-woo-odoo/fusion-woodoo/includes/class-webhooks.php new file mode 100644 index 00000000..585df702 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/includes/class-webhooks.php @@ -0,0 +1,106 @@ + self::WEBHOOK_PREFIX . 'order-created', + 'topic' => 'order.created', + 'api_url' => $odoo_url . '/woo/webhook/order', + ], + [ + 'name' => self::WEBHOOK_PREFIX . 'order-updated', + 'topic' => 'order.updated', + 'api_url' => $odoo_url . '/woo/webhook/order', + ], + [ + 'name' => self::WEBHOOK_PREFIX . 'product-updated', + 'topic' => 'product.updated', + 'api_url' => $odoo_url . '/woo/webhook/product', + ], + [ + 'name' => self::WEBHOOK_PREFIX . 'customer-created', + 'topic' => 'customer.created', + 'api_url' => $odoo_url . '/woo/webhook/customer', + ], + [ + 'name' => self::WEBHOOK_PREFIX . 'customer-updated', + 'topic' => 'customer.updated', + 'api_url' => $odoo_url . '/woo/webhook/customer', + ], + ]; + + // Remove stale webhooks first to avoid duplicates + self::unregister_all(); + + foreach ($webhooks as $wh_data) { + $webhook = new WC_Webhook(); + $webhook->set_name($wh_data['name']); + $webhook->set_topic($wh_data['topic']); + $webhook->set_delivery_url($wh_data['api_url']); + $webhook->set_secret(get_option('fusion_woodoo_api_key', wp_generate_password(32))); + $webhook->set_status('active'); + $webhook->save(); + } + } + + /** + * Delete all fusion-woodoo webhooks from WooCommerce. + */ + public static function unregister_all(): void { + $data_store = WC_Data_Store::load('webhook'); + $all_ids = $data_store->get_webhooks_ids('active'); + + foreach ($all_ids as $id) { + $webhook = wc_get_webhook($id); + if ($webhook && str_starts_with($webhook->get_name(), self::WEBHOOK_PREFIX)) { + $webhook->delete(true); + } + } + } + + /** + * Ping the Odoo URL and return whether it is reachable. + */ + public function check_stale(): bool { + $odoo_url = get_option('fusion_woodoo_odoo_url', ''); + if (empty($odoo_url)) { + return false; + } + + $response = wp_remote_get(rtrim($odoo_url, '/') . '/web/health', [ + 'timeout' => 10, + 'sslverify' => apply_filters('fusion_woodoo_sslverify', true), + ]); + + return !is_wp_error($response) && wp_remote_retrieve_response_code($response) < 500; + } + + /** + * Re-register webhooks when Odoo URL changes (triggered from settings). + */ + public function maybe_register(): void { + if ( + isset($_POST[Fusion_WooDoo_Admin_Settings::OPTION_ODOO_URL]) && + !empty($_POST[Fusion_WooDoo_Admin_Settings::OPTION_ODOO_URL]) + ) { + $this->register_webhooks(); + } + } +} diff --git a/fusion-woo-odoo/fusion-woodoo/readme.txt b/fusion-woo-odoo/fusion-woodoo/readme.txt new file mode 100644 index 00000000..42d66000 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/readme.txt @@ -0,0 +1,91 @@ +=== Fusion WooDoo === +Contributors: fusioncentral +Tags: woocommerce, odoo, integration, erp, orders, invoices, inventory, sync +Requires at least: 6.0 +Tested up to: 6.7 +Requires PHP: 8.0 +Stable tag: 1.0.0 +WC requires at least: 8.0 +WC tested up to: 9.0 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Seamless Odoo 19 integration for WooCommerce — sync orders, invoices, deliveries, and inventory in real time. + +== Description == + +Fusion WooDoo bridges your WooCommerce store with an Odoo 19 instance running the `fusion_woocommerce` module. It provides: + +* **Real-time order sync** via WooCommerce webhooks → Odoo +* **Status & tracking push-back** from Odoo → WooCommerce order meta +* **Customer portal tabs** in My Account: Sales Orders, Invoices, Deliveries, Returns +* **Visual order timeline** on the order detail page (Confirmed → Processing → Shipped → Delivered → Completed) +* **Invoice & delivery PDF storage** — Odoo pushes base64-encoded PDFs; customers download securely +* **Return requests** — customers submit returns from their account; routed to Odoo automatically +* **Odoo message thread** display on order detail pages +* **Admin settings page** under WooCommerce with one-click connection test and webhook status dashboard + +== Requirements == + +* WordPress 6.0 or higher +* WooCommerce 8.0 or higher +* PHP 8.0 or higher +* Odoo 19 with the `fusion_woocommerce` Odoo module installed and configured + +== Installation == + +1. Upload the `fusion-woodoo` folder to `/wp-content/plugins/`. +2. Activate the plugin through the **Plugins** screen in WordPress. +3. Go to **WooCommerce → Fusion WooDoo** in the WordPress admin. +4. Enter your Odoo base URL (e.g. `https://erp.example.com`) and the API key generated in the Odoo `fusion_woocommerce` module settings. +5. Save — webhooks will be registered automatically. +6. Click **Test Connection** to verify Odoo is reachable. + +== Frequently Asked Questions == + += Where do I get the API key? = + +In Odoo, open the `fusion_woocommerce` module settings and copy the generated API key. Paste it into **WooCommerce → Fusion WooDoo → API Key**. + += Which WooCommerce webhooks does the plugin register? = + +Five webhooks: `order.created`, `order.updated`, `product.updated`, `customer.created`, and `customer.updated`. All point to your Odoo instance. They are removed automatically on plugin deactivation. + += Where are invoice and delivery PDFs stored? = + +Under `wp-content/uploads/fusion-woodoo/invoices/` and `.../deliveries/`. Both directories are protected by `.htaccess` (deny from all). PDFs are served only to the order owner via a signed AJAX request. + += Can customers reorder from the Sales Orders tab? = + +Yes — the Reorder button adds all items from a previous order to the WooCommerce cart. + += How do I hide portal tabs I don't need? = + +Go to **WooCommerce → Fusion WooDoo** and uncheck the portal tab toggles you want to hide. + +== Screenshots == + +1. Admin settings page — connection details and webhook status. +2. My Account — Sales Orders tab. +3. My Account — Invoices tab with PDF download. +4. My Account — Deliveries tab with tracking links. +5. My Account — Returns tab with request form. +6. Order detail page — visual timeline. + +== Changelog == + += 1.0.0 = +* Initial release. +* Admin settings with connection test and webhook status. +* WP REST API endpoints: order/update, order/invoice, order/delivery, order/messages. +* WooCommerce webhook registration for orders, products, and customers. +* My Account portal tabs: Sales Orders, Invoices, Deliveries, Returns. +* Visual order timeline on order detail pages. +* Secure PDF download handler. +* Return request form with Odoo submission. +* Odoo message thread display. + +== Upgrade Notice == + += 1.0.0 = +Initial release — no upgrade steps required. diff --git a/fusion-woo-odoo/fusion-woodoo/templates/admin/settings.php b/fusion-woo-odoo/fusion-woodoo/templates/admin/settings.php new file mode 100644 index 00000000..6e0c81bf --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/admin/settings.php @@ -0,0 +1,85 @@ +check_stale() : false; +?> +
+

+ +
+

+
+ +
+
+ +
+
+ +
+

+

+ + +

+

+ + + + + + +

+ + +
+ +
+

+

+ get_webhooks_ids('active'); + $wh_list = []; + foreach ($all_ids as $id) { + $wh = wc_get_webhook($id); + if ($wh && str_starts_with($wh->get_name(), 'fusion-woodoo-')) { + $wh_list[] = $wh; + } + } + ?> + +

+ + + + + + + + + + + + + + + + + + + + +
get_name()); ?>get_topic()); ?>get_delivery_url()); ?> + + get_status())); ?> + +
+ +
+
diff --git a/fusion-woo-odoo/fusion-woodoo/templates/my-account/communication.php b/fusion-woo-odoo/fusion-woodoo/templates/my-account/communication.php new file mode 100644 index 00000000..e61f58f3 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/my-account/communication.php @@ -0,0 +1,36 @@ +get_meta('_odoo_messages'); +if (empty($messages) || !is_array($messages)) { + return; +} +?> +
+

+
+ +
+
+ + + + + + + +
+
+ +
+
+ +
+
diff --git a/fusion-woo-odoo/fusion-woodoo/templates/my-account/deliveries.php b/fusion-woo-odoo/fusion-woodoo/templates/my-account/deliveries.php new file mode 100644 index 00000000..a7cc24c2 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/my-account/deliveries.php @@ -0,0 +1,105 @@ +request('/woo/api/delivery/list', ['customer_id' => get_current_user_id()]); +$deliveries = $result['success'] ? ($result['data']['deliveries'] ?? []) : []; + +// Also pull locally stored delivery PDFs +$wc_orders = wc_get_orders([ + 'customer' => get_current_user_id(), + 'limit' => 50, + 'meta_key' => '_odoo_delivery_pdf', +]); +?> +
+

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + get_meta('_odoo_tracking_number'); + $shipping_carrier = $order->get_meta('_odoo_shipping_carrier'); + ?> + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
get_order_number()); ?>get_date_completed() ? $order->get_date_completed()->date_i18n('Y-m-d') : '—'); ?>get_status())); ?> + get_meta('_odoo_delivery_pdf'))): ?> + + + + + + +
+ +
diff --git a/fusion-woo-odoo/fusion-woodoo/templates/my-account/invoices.php b/fusion-woo-odoo/fusion-woodoo/templates/my-account/invoices.php new file mode 100644 index 00000000..088b589a --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/my-account/invoices.php @@ -0,0 +1,88 @@ +request('/woo/api/invoice/list', ['customer_id' => get_current_user_id()]); +$invoices = $result['success'] ? ($result['data']['invoices'] ?? []) : []; + +// Also pull locally stored invoices from WC orders +$wc_orders = wc_get_orders([ + 'customer' => get_current_user_id(), + 'limit' => 50, + 'meta_key' => '_odoo_invoice_pdf', +]); +?> +
+

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + get_id(), array_column($invoices, 'wc_order_id'))) continue; + $has_pdf = !empty($order->get_meta('_odoo_invoice_pdf')); + if (!$has_pdf) continue; + ?> + + + + + + + + + + +
+ + + + + + + + + + + +
get_order_number()); ?>get_date_created() ? $order->get_date_created()->date_i18n('Y-m-d') : '—'); ?>get_total(), 2); ?>get_status())); ?> + + + +
+ +
diff --git a/fusion-woo-odoo/fusion-woodoo/templates/my-account/order-timeline.php b/fusion-woo-odoo/fusion-woodoo/templates/my-account/order-timeline.php new file mode 100644 index 00000000..1f27c642 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/my-account/order-timeline.php @@ -0,0 +1,49 @@ + +
+

+
+ $label): + $idx = array_search($key, array_keys($stages)); + $is_done = $idx < $current_index; + $is_active = $idx === $current_index; + $state_class = $is_done ? 'done' : ($is_active ? 'active' : 'pending'); + ?> +
+
+ + + +
+
+ + + + + + + + — + + + + +
+
+ +
+ + +
+
diff --git a/fusion-woo-odoo/fusion-woodoo/templates/my-account/returns.php b/fusion-woo-odoo/fusion-woodoo/templates/my-account/returns.php new file mode 100644 index 00000000..0e380313 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/my-account/returns.php @@ -0,0 +1,99 @@ +get_customer_orders(); +$existing = $returns_handler->get_existing_returns(); +?> +
+

+ + +

+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + +

+ + +

+ + + +
+ + +
+ + +
+ + + +
+ + +
+ + +
+ + + +
diff --git a/fusion-woo-odoo/fusion-woodoo/templates/my-account/sales-orders.php b/fusion-woo-odoo/fusion-woodoo/templates/my-account/sales-orders.php new file mode 100644 index 00000000..843004a0 --- /dev/null +++ b/fusion-woo-odoo/fusion-woodoo/templates/my-account/sales-orders.php @@ -0,0 +1,56 @@ +request('/woo/api/sales/list', ['customer_id' => get_current_user_id()]); +$orders = $result['success'] ? ($result['data']['orders'] ?? []) : []; +?> +
+

+ + +

+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+

+
+ +