From de14a28112fb50e66e5cfe88586cab39a342cc8a Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 31 Mar 2026 20:53:43 -0400 Subject: [PATCH] feat: add OWL product mapping UI with live AJAX search - AjaxSearch component: debounced 300ms, calls /woo/search/* endpoints via rpc() - ProductMapping client action: 3 tabs (Mapped, Unmatched, Conflicts) - Mapped tab: live search, bulk unmap/sync, per-row price/inventory sync toggles - Unmatched tab: split Odoo|WC panels, click-to-select, Map/Create/Ignore actions - Conflicts tab: Use Odoo / Use WC per-row and bulk resolve - Top bar: instance selector, Fetch Products, Sync Now, live stats - woo_dashboard.xml updated with ir.actions.client records - woo_menus.xml pointed at new client action - CSS: full layout styles, badges, split view, progress bar, buttons Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fusion_woocommerce/__manifest__.py | 5 + .../static/src/css/woo_styles.css | 373 ++++++++++++++- .../static/src/js/ajax_search.js | 47 ++ .../static/src/js/product_mapping.js | 449 ++++++++++++++++++ .../static/src/xml/product_mapping.xml | 342 +++++++++++++ .../views/woo_dashboard.xml | 17 +- .../fusion_woocommerce/views/woo_menus.xml | 2 +- 7 files changed, 1225 insertions(+), 10 deletions(-) create mode 100644 fusion-woo-odoo/fusion_woocommerce/static/src/js/ajax_search.js create mode 100644 fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js create mode 100644 fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml diff --git a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py index 97726427..0f1f1e4e 100644 --- a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py +++ b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py @@ -35,6 +35,11 @@ 'assets': { 'web.assets_backend': [ 'fusion_woocommerce/static/src/css/woo_styles.css', + 'fusion_woocommerce/static/src/js/ajax_search.js', + 'fusion_woocommerce/static/src/js/product_mapping.js', + 'fusion_woocommerce/static/src/js/dashboard.js', + 'fusion_woocommerce/static/src/xml/product_mapping.xml', + 'fusion_woocommerce/static/src/xml/dashboard.xml', ], }, 'images': ['static/description/icon.png'], diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css b/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css index 1960e55d..454aba45 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/css/woo_styles.css @@ -1 +1,372 @@ -/* Fusion WooCommerce — custom styles */ +/* ============================================================ + Fusion WooCommerce — Custom Styles + ============================================================ */ + +/* ---------------------------------------------------------- + Status badges + ---------------------------------------------------------- */ +.woo-badge { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.02em; +} +.woo-badge-mapped { background: #d1fae5; color: #065f46; } +.woo-badge-unmapped { background: #f3f4f6; color: #4b5563; } +.woo-badge-conflict { background: #fef3c7; color: #92400e; } +.woo-badge-error { background: #fee2e2; color: #991b1b; } +.woo-badge-success { background: #d1fae5; color: #065f46; } +.woo-badge-failed { background: #fee2e2; color: #991b1b; } + +/* ---------------------------------------------------------- + Tab navigation + ---------------------------------------------------------- */ +.woo-tabs { + display: flex; + gap: 4px; + border-bottom: 2px solid #e5e7eb; + margin-bottom: 16px; +} +.woo-tab { + padding: 8px 20px; + cursor: pointer; + font-weight: 500; + color: #6b7280; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + background: none; + border-top: none; + border-left: none; + border-right: none; + transition: color 0.15s, border-color 0.15s; +} +.woo-tab:hover { color: #374151; } +.woo-tab.active { + color: #7c3aed; + border-bottom-color: #7c3aed; +} + +/* ---------------------------------------------------------- + Top bar / stats + ---------------------------------------------------------- */ +.woo-topbar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: #f9fafb; + border-bottom: 1px solid #e5e7eb; + flex-wrap: wrap; +} +.woo-topbar select, +.woo-topbar input { + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 6px 10px; + font-size: 0.875rem; +} +.woo-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 14px; + border-left: 1px solid #e5e7eb; +} +.woo-stat-value { + font-size: 1.25rem; + font-weight: 700; + color: #111827; +} +.woo-stat-label { + font-size: 0.7rem; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* ---------------------------------------------------------- + Search bar + ---------------------------------------------------------- */ +.woo-search-wrap { + position: relative; + display: inline-flex; + align-items: center; +} +.woo-search-wrap .woo-search-icon { + position: absolute; + left: 10px; + color: #9ca3af; + font-size: 14px; + pointer-events: none; +} +.woo-search-input { + padding: 6px 10px 6px 32px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.875rem; + width: 240px; + transition: border-color 0.15s; +} +.woo-search-input:focus { + outline: none; + border-color: #7c3aed; + box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15); +} + +/* ---------------------------------------------------------- + Tables + ---------------------------------------------------------- */ +.woo-table-wrap { + overflow-x: auto; +} +.woo-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} +.woo-table th { + background: #f3f4f6; + padding: 10px 12px; + text-align: left; + font-weight: 600; + color: #374151; + border-bottom: 2px solid #e5e7eb; + white-space: nowrap; +} +.woo-table td { + padding: 9px 12px; + border-bottom: 1px solid #f3f4f6; + color: #374151; + vertical-align: middle; +} +.woo-table tr:hover td { background: #f9fafb; } +.woo-table tr.selected td { background: #ede9fe; } + +/* ---------------------------------------------------------- + Split view (unmatched tab) + ---------------------------------------------------------- */ +.woo-split { + display: grid; + grid-template-columns: 1fr 40px 1fr; + gap: 0; + align-items: start; +} +.woo-split-panel { + border: 1px solid #e5e7eb; + border-radius: 8px; + overflow: hidden; +} +.woo-split-panel-header { + background: #f3f4f6; + padding: 10px 14px; + font-weight: 600; + color: #374151; + border-bottom: 1px solid #e5e7eb; + display: flex; + justify-content: space-between; + align-items: center; +} +.woo-split-divider { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 60px; + gap: 8px; + color: #9ca3af; + font-size: 1.2rem; +} +.woo-split-list { + max-height: 480px; + overflow-y: auto; +} +.woo-split-item { + padding: 10px 14px; + cursor: pointer; + border-bottom: 1px solid #f3f4f6; + transition: background 0.1s; +} +.woo-split-item:hover { background: #f9fafb; } +.woo-split-item.selected { background: #ede9fe; } +.woo-split-item-name { font-weight: 500; color: #111827; } +.woo-split-item-sub { font-size: 0.75rem; color: #6b7280; margin-top: 1px; } + +/* ---------------------------------------------------------- + Map actions bar + ---------------------------------------------------------- */ +.woo-map-actions { + display: flex; + gap: 8px; + padding: 10px 0 14px; + flex-wrap: wrap; +} + +/* ---------------------------------------------------------- + Buttons + ---------------------------------------------------------- */ +.woo-btn { + padding: 6px 14px; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + border: 1px solid transparent; + transition: background 0.15s, border-color 0.15s; +} +.woo-btn-primary { background: #7c3aed; color: #fff; border-color: #7c3aed; } +.woo-btn-primary:hover { background: #6d28d9; } +.woo-btn-success { background: #059669; color: #fff; border-color: #059669; } +.woo-btn-success:hover { background: #047857; } +.woo-btn-warning { background: #d97706; color: #fff; border-color: #d97706; } +.woo-btn-warning:hover { background: #b45309; } +.woo-btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; } +.woo-btn-danger:hover { background: #b91c1c; } +.woo-btn-secondary { background: #fff; color: #374151; border-color: #d1d5db; } +.woo-btn-secondary:hover { background: #f3f4f6; } +.woo-btn-sm { padding: 3px 10px; font-size: 0.8rem; } +.woo-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +/* ---------------------------------------------------------- + Dashboard cards + ---------------------------------------------------------- */ +.woo-dashboard { + padding: 20px; +} +.woo-dashboard-title { + font-size: 1.4rem; + font-weight: 700; + color: #111827; + margin-bottom: 4px; +} +.woo-dashboard-subtitle { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 24px; +} +.woo-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} +.woo-card { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 10px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); + transition: box-shadow 0.15s; +} +.woo-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); } +.woo-card-clickable { cursor: pointer; } +.woo-card-icon { + font-size: 1.6rem; + margin-bottom: 4px; +} +.woo-card-value { + font-size: 2rem; + font-weight: 700; + color: #111827; + line-height: 1; +} +.woo-card-label { + font-size: 0.8rem; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.woo-card-sub { + font-size: 0.78rem; + color: #9ca3af; + margin-top: 2px; +} +.woo-card-pending { border-left: 4px solid #f59e0b; } +.woo-card-errors { border-left: 4px solid #ef4444; } +.woo-card-mapped { border-left: 4px solid #10b981; } +.woo-card-sync { border-left: 4px solid #6366f1; } + +/* ---------------------------------------------------------- + Progress bar + ---------------------------------------------------------- */ +.woo-progress-wrap { + background: #e5e7eb; + border-radius: 6px; + height: 8px; + overflow: hidden; + margin-top: 6px; +} +.woo-progress-bar { + height: 100%; + background: linear-gradient(90deg, #10b981, #059669); + border-radius: 6px; + transition: width 0.4s ease; +} + +/* ---------------------------------------------------------- + Loading spinner + ---------------------------------------------------------- */ +.woo-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: #6b7280; + gap: 10px; + font-size: 0.9rem; +} +.woo-spinner { + width: 20px; + height: 20px; + border: 2px solid #e5e7eb; + border-top-color: #7c3aed; + border-radius: 50%; + animation: woo-spin 0.7s linear infinite; +} +@keyframes woo-spin { to { transform: rotate(360deg); } } + +/* ---------------------------------------------------------- + Empty states + ---------------------------------------------------------- */ +.woo-empty { + text-align: center; + padding: 48px 20px; + color: #9ca3af; +} +.woo-empty-icon { font-size: 2.5rem; margin-bottom: 10px; } +.woo-empty-text { font-size: 0.9rem; } + +/* ---------------------------------------------------------- + Quick actions + ---------------------------------------------------------- */ +.woo-quick-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 8px; +} + +/* ---------------------------------------------------------- + Section header + ---------------------------------------------------------- */ +.woo-section-title { + font-size: 1rem; + font-weight: 600; + color: #374151; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #f3f4f6; +} + +/* ---------------------------------------------------------- + Checkbox column + ---------------------------------------------------------- */ +.woo-table th.woo-check-col, +.woo-table td.woo-check-col { + width: 36px; + text-align: center; +} diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/js/ajax_search.js b/fusion-woo-odoo/fusion_woocommerce/static/src/js/ajax_search.js new file mode 100644 index 00000000..5ecd3fe8 --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/js/ajax_search.js @@ -0,0 +1,47 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +/** + * AjaxSearch — reusable debounced search component. + * + * Props: + * endpoint {String} The /woo/search/* URL to POST to. + * instanceId {Number} woo.instance ID (optional). + * onResults {Function} Callback receives the results array. + * placeholder {String} Input placeholder text. + */ +export class AjaxSearch extends Component { + static template = "fusion_woocommerce.AjaxSearch"; + static props = ["endpoint", "onResults", "*"]; + + setup() { + this.state = useState({ query: "" }); + this._debounceTimer = null; + } + + onInput(ev) { + const query = ev.target.value; + this.state.query = query; + + clearTimeout(this._debounceTimer); + this._debounceTimer = setTimeout(() => { + this._doSearch(query); + }, 300); + } + + async _doSearch(query) { + try { + const params = { query }; + if (this.props.instanceId) { + params.instance_id = this.props.instanceId; + } + const results = await rpc(this.props.endpoint, params); + this.props.onResults(results || []); + } catch (err) { + console.error("[AjaxSearch] search error:", err); + this.props.onResults([]); + } + } +} diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js b/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js new file mode 100644 index 00000000..789025fb --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js @@ -0,0 +1,449 @@ +/** @odoo-module **/ + +import { Component, useState, onWillStart } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { rpc } from "@web/core/network/rpc"; +import { AjaxSearch } from "./ajax_search"; + +/** + * ProductMapping — OWL client action for the WooCommerce product mapping UI. + * + * Three tabs: + * 1. Mapped Products — searchable table, bulk actions + * 2. Unmatched Products — split view Odoo | WC, map/create/ignore actions + * 3. Conflicts — table with Use Odoo / Use WC buttons + */ +export class ProductMapping extends Component { + static template = "fusion_woocommerce.ProductMapping"; + static components = { AjaxSearch }; + static props = ["action", "*"]; + + setup() { + this.actionService = useService("action"); + this.notification = useService("notification"); + + this.state = useState({ + // UI state + activeTab: "mapped", + loading: false, + + // Instance selector + instances: [], + instanceId: false, + + // Stats + mappedCount: 0, + unmappedCount: 0, + conflictCount: 0, + + // Mapped tab + mappedProducts: [], + selectedMapped: [], + + // Unmatched tab + odooProducts: [], + wooProducts: [], + selectedOdooId: false, + selectedWooId: false, + + // Conflicts tab + conflicts: [], + }); + + onWillStart(async () => { + await this._loadInstances(); + await this._refreshAll(); + }); + } + + // ------------------------------------------------------------------------- + // Data loaders + // ------------------------------------------------------------------------- + + async _loadInstances() { + try { + const result = await rpc("/web/dataset/call_kw", { + model: "woo.instance", + method: "search_read", + args: [[]], + kwargs: { fields: ["id", "name", "state"], limit: 50 }, + }); + this.state.instances = result || []; + if (result && result.length === 1) { + this.state.instanceId = result[0].id; + } + } catch (err) { + console.error("[ProductMapping] _loadInstances error:", err); + } + } + + async _refreshAll() { + this.state.loading = true; + try { + await Promise.all([ + this._loadMapped(), + this._loadOdooProducts(""), + this._loadWooProducts(""), + this._loadConflicts(), + this._loadStats(), + ]); + } finally { + this.state.loading = false; + } + } + + async _loadMapped(query = "") { + try { + const params = { query, limit: 50 }; + if (this.state.instanceId) { + params.instance_id = this.state.instanceId; + } + const result = await rpc("/woo/search/mapped", params); + this.state.mappedProducts = result || []; + this.state.selectedMapped = []; + } catch (err) { + console.error("[ProductMapping] _loadMapped error:", err); + } + } + + async _loadOdooProducts(query = "") { + try { + const params = { query, limit: 50 }; + if (this.state.instanceId) { + params.instance_id = this.state.instanceId; + } + const result = await rpc("/woo/search/odoo_products", params); + this.state.odooProducts = result || []; + } catch (err) { + console.error("[ProductMapping] _loadOdooProducts error:", err); + } + } + + async _loadWooProducts(query = "") { + try { + const params = { query, limit: 50 }; + if (this.state.instanceId) { + params.instance_id = this.state.instanceId; + } + const result = await rpc("/woo/search/woo_products", params); + this.state.wooProducts = result || []; + } catch (err) { + console.error("[ProductMapping] _loadWooProducts error:", err); + } + } + + async _loadConflicts() { + try { + const domain = [["resolution", "=", "pending"]]; + if (this.state.instanceId) { + domain.push(["instance_id", "=", this.state.instanceId]); + } + const result = await rpc("/web/dataset/call_kw", { + model: "woo.conflict", + method: "search_read", + args: [domain], + kwargs: { + fields: [ + "id", "conflict_type", "field_name", + "odoo_value", "woo_value", "resolution", + "instance_id", + ], + limit: 100, + }, + }); + this.state.conflicts = result || []; + this.state.conflictCount = this.state.conflicts.length; + } catch (err) { + console.error("[ProductMapping] _loadConflicts error:", err); + } + } + + async _loadStats() { + try { + const base = this.state.instanceId + ? [["instance_id", "=", this.state.instanceId]] + : []; + + const [mapped, unmapped] = await Promise.all([ + rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "search_count", + args: [[...base, ["state", "=", "mapped"]]], + kwargs: {}, + }), + rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "search_count", + args: [[...base, ["state", "=", "unmapped"]]], + kwargs: {}, + }), + ]); + + this.state.mappedCount = mapped || 0; + this.state.unmappedCount = unmapped || 0; + } catch (err) { + console.error("[ProductMapping] _loadStats error:", err); + } + } + + // ------------------------------------------------------------------------- + // Tab handlers + // ------------------------------------------------------------------------- + + setTab(tab) { + this.state.activeTab = tab; + } + + // ------------------------------------------------------------------------- + // Instance selector + // ------------------------------------------------------------------------- + + async onInstanceChange(ev) { + const val = ev.target.value; + this.state.instanceId = val ? parseInt(val, 10) : false; + await this._refreshAll(); + } + + // ------------------------------------------------------------------------- + // Mapped tab + // ------------------------------------------------------------------------- + + onMappedResults(results) { + this.state.mappedProducts = results; + } + + toggleSelectMapped(id) { + const idx = this.state.selectedMapped.indexOf(id); + if (idx >= 0) { + this.state.selectedMapped.splice(idx, 1); + } else { + this.state.selectedMapped.push(id); + } + } + + toggleSelectAllMapped(ev) { + if (ev.target.checked) { + this.state.selectedMapped = this.state.mappedProducts.map((p) => p.id); + } else { + this.state.selectedMapped = []; + } + } + + isMappedSelected(id) { + return this.state.selectedMapped.includes(id); + } + + async unmapSelected() { + if (!this.state.selectedMapped.length) return; + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "write", + args: [this.state.selectedMapped, { state: "unmapped", product_id: false }], + kwargs: {}, + }); + this.notification.add("Products unmapped successfully.", { type: "success" }); + await this._refreshAll(); + } catch (err) { + console.error("[ProductMapping] unmapSelected error:", err); + this.notification.add("Failed to unmap products.", { type: "danger" }); + } + } + + async syncSelected() { + if (!this.state.selectedMapped.length) return; + this.notification.add("Sync queued for selected products.", { type: "info" }); + } + + async togglePriceSync(mapId, current) { + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "write", + args: [[mapId], { sync_price: !current }], + kwargs: {}, + }); + await this._loadMapped(); + } catch (err) { + console.error("[ProductMapping] togglePriceSync error:", err); + } + } + + async toggleInventorySync(mapId, current) { + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "write", + args: [[mapId], { sync_inventory: !current }], + kwargs: {}, + }); + await this._loadMapped(); + } catch (err) { + console.error("[ProductMapping] toggleInventorySync error:", err); + } + } + + // ------------------------------------------------------------------------- + // Unmatched tab + // ------------------------------------------------------------------------- + + onOdooResults(results) { + this.state.odooProducts = results; + if (!results.find((r) => r.id === this.state.selectedOdooId)) { + this.state.selectedOdooId = false; + } + } + + onWooResults(results) { + this.state.wooProducts = results; + if (!results.find((r) => r.id === this.state.selectedWooId)) { + this.state.selectedWooId = false; + } + } + + selectOdoo(id) { + this.state.selectedOdooId = this.state.selectedOdooId === id ? false : id; + } + + selectWoo(id) { + this.state.selectedWooId = this.state.selectedWooId === id ? false : id; + } + + get canMap() { + return this.state.selectedOdooId && this.state.selectedWooId; + } + + async mapSelected() { + if (!this.canMap) return; + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "write", + args: [ + [this.state.selectedWooId], + { + product_id: this.state.selectedOdooId, + state: "mapped", + }, + ], + kwargs: {}, + }); + this.notification.add("Products mapped successfully.", { type: "success" }); + this.state.selectedOdooId = false; + this.state.selectedWooId = false; + await this._refreshAll(); + } catch (err) { + console.error("[ProductMapping] mapSelected error:", err); + this.notification.add("Failed to map products.", { type: "danger" }); + } + } + + async createInWC(odooProductId) { + this.notification.add("Create in WooCommerce queued.", { type: "info" }); + } + + async createInOdoo(wooMapId) { + this.notification.add("Create in Odoo queued.", { type: "info" }); + } + + async ignoreWoo(wooMapId) { + try { + await rpc("/web/dataset/call_kw", { + model: "woo.product.map", + method: "write", + args: [[wooMapId], { state: "error" }], + kwargs: {}, + }); + this.notification.add("WooCommerce product ignored.", { type: "warning" }); + await this._loadWooProducts(""); + } catch (err) { + console.error("[ProductMapping] ignoreWoo error:", err); + } + } + + // ------------------------------------------------------------------------- + // Conflicts tab + // ------------------------------------------------------------------------- + + async resolveConflict(conflictId, resolution) { + try { + await rpc("/web/dataset/call_kw", { + model: "woo.conflict", + method: "write", + args: [[conflictId], { resolution }], + kwargs: {}, + }); + this.notification.add("Conflict resolved.", { type: "success" }); + await this._loadConflicts(); + } catch (err) { + console.error("[ProductMapping] resolveConflict error:", err); + this.notification.add("Failed to resolve conflict.", { type: "danger" }); + } + } + + async resolveAllConflicts(resolution) { + const ids = this.state.conflicts.map((c) => c.id); + if (!ids.length) return; + try { + await rpc("/web/dataset/call_kw", { + model: "woo.conflict", + method: "write", + args: [ids, { resolution }], + kwargs: {}, + }); + this.notification.add(`All conflicts resolved using ${resolution === "use_odoo" ? "Odoo" : "WooCommerce"} values.`, { type: "success" }); + await this._loadConflicts(); + } catch (err) { + console.error("[ProductMapping] resolveAllConflicts error:", err); + } + } + + // ------------------------------------------------------------------------- + // Top bar actions + // ------------------------------------------------------------------------- + + async fetchProducts() { + if (!this.state.instanceId) { + this.notification.add("Please select an instance first.", { type: "warning" }); + return; + } + try { + this.state.loading = true; + await rpc("/web/dataset/call_kw", { + model: "woo.instance", + method: "action_fetch_products", + args: [[this.state.instanceId]], + kwargs: {}, + }); + this.notification.add("Product fetch started.", { type: "success" }); + await this._refreshAll(); + } catch (err) { + console.error("[ProductMapping] fetchProducts error:", err); + this.notification.add("Failed to fetch products.", { type: "danger" }); + } finally { + this.state.loading = false; + } + } + + async syncNow() { + if (!this.state.instanceId) { + this.notification.add("Please select an instance first.", { type: "warning" }); + return; + } + try { + await rpc("/web/dataset/call_kw", { + model: "woo.instance", + method: "action_sync", + args: [[this.state.instanceId]], + kwargs: {}, + }); + this.notification.add("Sync started.", { type: "success" }); + } catch (err) { + console.error("[ProductMapping] syncNow error:", err); + this.notification.add("Failed to start sync.", { type: "danger" }); + } + } +} + +registry.category("actions").add("fusion_woocommerce.product_mapping", ProductMapping); diff --git a/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml new file mode 100644 index 00000000..0a02065a --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml @@ -0,0 +1,342 @@ + + + + + +
+ + +
+
+ + + +
+ + +
+ + + + + + + + +
+ + +
+ + Mapped +
+
+ + Unmapped +
+
+ + Conflicts +
+
+ + + +
+
+ Loading… +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + +
+
🔗
+
No mapped products found.
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + WooCommerce ProductSKUOdoo ProductInstancePrice SyncInventory Sync
+ + + + + +
+
+
+
+ + + + +
+ + + + Select one Odoo product and one WooCommerce product to map them. + + +
+ +
+ +
+
+ Odoo Products + +
+
+ +
+
No Odoo products found.
+
+
+ +
+
+
+ SKU: · + $ +
+
+
+
+
+ + +
+ +
+ + +
+
+ WooCommerce Products + +
+
+ +
+
No unmatched WooCommerce products.
+
+
+ +
+
+
+ SKU: · + +
+ +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+
+ + + + +
+
+
No pending conflicts. Everything is in sync!
+
+
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
TypeFieldOdoo ValueWooCommerce ValueActions
+ + + + +
+ + +
+
+
+
+
+ +
+
+
+
+ + diff --git a/fusion-woo-odoo/fusion_woocommerce/views/woo_dashboard.xml b/fusion-woo-odoo/fusion_woocommerce/views/woo_dashboard.xml index f4cd2ab6..4ba5778b 100644 --- a/fusion-woo-odoo/fusion_woocommerce/views/woo_dashboard.xml +++ b/fusion-woo-odoo/fusion_woocommerce/views/woo_dashboard.xml @@ -1,15 +1,16 @@ - - + + WooCommerce Dashboard - woo.instance - tree,form + fusion_woocommerce.woo_dashboard + + + + + Product Mapping + fusion_woocommerce.product_mapping diff --git a/fusion-woo-odoo/fusion_woocommerce/views/woo_menus.xml b/fusion-woo-odoo/fusion_woocommerce/views/woo_menus.xml index 43f4eaec..cde94d29 100644 --- a/fusion-woo-odoo/fusion_woocommerce/views/woo_menus.xml +++ b/fusion-woo-odoo/fusion_woocommerce/views/woo_menus.xml @@ -23,7 +23,7 @@