From 52be90c10d06299cdd02095fa12b337b9f851bbb Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 1 Apr 2026 15:56:13 -0400 Subject: [PATCH] feat: category filter dropdown on unmatched Odoo products panel Filter by Odoo product category or clear filter. Backend supports both include and exclude category filtering. Loads all categories on init for the dropdown. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controllers/product_search.py | 31 +++++++++- .../static/src/css/woo_styles.css | 15 +++++ .../static/src/js/product_mapping.js | 57 +++++++++++++++++++ .../static/src/xml/product_mapping.xml | 18 +++++- 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py b/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py index 5ce89524..57173896 100644 --- a/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py +++ b/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py @@ -21,7 +21,8 @@ class WooProductSearchController(http.Controller): '/woo/search/odoo_products', type='jsonrpc', auth='user', methods=['POST'], ) - def search_odoo_products(self, query='', instance_id=None, limit=20, offset=0, **kw): + def search_odoo_products(self, query='', instance_id=None, limit=20, offset=0, + category_id=None, exclude_category_ids=None, **kw): """ Search Odoo products by name or internal reference (SKU). @@ -30,6 +31,8 @@ class WooProductSearchController(http.Controller): instance_id (int): woo.instance ID (used for future per-instance filtering). limit (int): Max results to return (default 20). offset (int): Offset for pagination (default 0). + category_id (int): Filter by Odoo product category. + exclude_category_ids (list): Exclude these category IDs. Returns: dict with 'results' list and 'total' count @@ -45,6 +48,19 @@ class WooProductSearchController(http.Controller): ('default_code', 'ilike', query), ] + if category_id: + domain.append(('categ_id', '=', int(category_id))) + + if exclude_category_ids: + if isinstance(exclude_category_ids, str): + import json as _json + try: + exclude_category_ids = _json.loads(exclude_category_ids) + except (ValueError, TypeError): + exclude_category_ids = [] + if exclude_category_ids: + domain.append(('categ_id', 'not in', [int(x) for x in exclude_category_ids])) + total = request.env['product.product'].search_count(domain) products = request.env['product.product'].search(domain, limit=limit, offset=offset) @@ -56,6 +72,7 @@ class WooProductSearchController(http.Controller): 'default_code': p.default_code or '', 'list_price': p.list_price, 'qty_available': p.qty_available, + 'categ_name': p.categ_id.name if p.categ_id else '', } for p in products ], @@ -110,6 +127,18 @@ class WooProductSearchController(http.Controller): 'total': total, } + @http.route( + '/woo/search/odoo_categories', + type='jsonrpc', auth='user', methods=['POST'], + ) + def get_odoo_categories(self, **kw): + """Return all Odoo product categories for filtering.""" + categories = request.env['product.category'].search([], order='complete_name') + return [ + {'id': c.id, 'name': c.name, 'complete_name': c.complete_name} + for c in categories + ] + @http.route( '/woo/search/mapped', type='jsonrpc', auth='user', methods=['POST'], 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 2aa8caf8..fa780b99 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 @@ -644,6 +644,21 @@ html[style*="color-scheme: dark"] { box-shadow: 0 0 0 2px var(--woo-accent-glow); } +/* Category filter dropdown */ +.woo-filter-select { + padding: 3px 8px; + border: 1px solid var(--woo-input-border); + border-radius: 4px; + font-size: 0.8rem; + background: var(--woo-input-bg); + color: var(--woo-text-primary); + max-width: 200px; +} +.woo-filter-select option { + background: var(--woo-bg-primary); + color: var(--woo-text-primary); +} + .woo-edit-input-text { text-align: left; width: 120px; 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 index 7d5a3e42..f9100bef 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/js/product_mapping.js @@ -60,12 +60,18 @@ export class ProductMapping extends Component { // Pagination pageSize: 50, + // Category filters + odooCategories: [], + odooFilterCategoryId: false, + odooExcludeCategoryIds: [], + // Conflicts tab conflicts: [], }); onWillStart(async () => { await this._loadInstances(); + await this._loadOdooCategories(); await this._refreshAll(); }); } @@ -125,6 +131,15 @@ export class ProductMapping extends Component { } } + async _loadOdooCategories() { + try { + const result = await rpc("/woo/search/odoo_categories", {}); + this.state.odooCategories = result || []; + } catch (err) { + console.error("[ProductMapping] _loadOdooCategories error:", err); + } + } + async _loadOdooProducts(query = "") { try { const params = { @@ -135,6 +150,12 @@ export class ProductMapping extends Component { if (this.state.instanceId) { params.instance_id = this.state.instanceId; } + if (this.state.odooFilterCategoryId) { + params.category_id = this.state.odooFilterCategoryId; + } + if (this.state.odooExcludeCategoryIds.length) { + params.exclude_category_ids = JSON.stringify(this.state.odooExcludeCategoryIds); + } const result = await rpc("/woo/search/odoo_products", params); this.state.odooProducts = (result && result.results) || []; this.state.unmatchedOdooTotal = (result && result.total) || 0; @@ -260,6 +281,42 @@ export class ProductMapping extends Component { await this._refreshAll(); } + // ------------------------------------------------------------------------- + // Category filter + // ------------------------------------------------------------------------- + + async onOdooCategoryFilter(ev) { + const val = ev.target.value; + this.state.odooFilterCategoryId = val ? parseInt(val, 10) : false; + this.state.unmatchedOdooPage = 1; + await this._loadOdooProducts(""); + } + + toggleExcludeCategory(catId) { + const idx = this.state.odooExcludeCategoryIds.indexOf(catId); + if (idx >= 0) { + this.state.odooExcludeCategoryIds.splice(idx, 1); + } else { + this.state.odooExcludeCategoryIds.push(catId); + } + } + + async applyExcludeCategories() { + this.state.unmatchedOdooPage = 1; + await this._loadOdooProducts(""); + } + + isCategoryExcluded(catId) { + return this.state.odooExcludeCategoryIds.includes(catId); + } + + async clearCategoryFilter() { + this.state.odooFilterCategoryId = false; + this.state.odooExcludeCategoryIds = []; + this.state.unmatchedOdooPage = 1; + await this._loadOdooProducts(""); + } + // ------------------------------------------------------------------------- // Mapped tab // ------------------------------------------------------------------------- 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 index af80bc82..f4bdbe0d 100644 --- a/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml +++ b/fusion-woo-odoo/fusion_woocommerce/static/src/xml/product_mapping.xml @@ -333,8 +333,24 @@
-
+
Odoo Products +
+ + + + +