From 5e806745da91de4eeef1967278a6ad2d366ab812 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 1 Apr 2026 16:06:15 -0400 Subject: [PATCH] feat: persistent hidden categories with wizard and toggle Categories to hide are stored on woo.instance and persist across sessions. Click 'Hidden (N)' button to open wizard where you can add/remove categories using a tag picker. Eye/eye-slash toggle to quickly apply or unapply the filter without losing the saved list. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fusion_woocommerce/__manifest__.py | 1 + .../controllers/product_search.py | 9 ++- .../fusion_woocommerce/models/woo_instance.py | 4 + .../security/ir.model.access.csv | 1 + .../static/src/js/product_mapping.js | 74 ++++++++++--------- .../static/src/xml/product_mapping.xml | 24 +++--- .../fusion_woocommerce/wizard/__init__.py | 1 + .../wizard/woo_category_filter.py | 42 +++++++++++ .../wizard/woo_category_filter_views.xml | 30 ++++++++ 9 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter.py create mode 100644 fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter_views.xml diff --git a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py index ed413f72..c55facda 100644 --- a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py +++ b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py @@ -33,6 +33,7 @@ 'wizard/woo_setup_wizard_views.xml', 'wizard/woo_product_fetch_views.xml', 'wizard/woo_product_create_views.xml', + 'wizard/woo_category_filter_views.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py b/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py index 57173896..8bba4dfe 100644 --- a/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py +++ b/fusion-woo-odoo/fusion_woocommerce/controllers/product_search.py @@ -22,7 +22,8 @@ class WooProductSearchController(http.Controller): type='jsonrpc', auth='user', methods=['POST'], ) def search_odoo_products(self, query='', instance_id=None, limit=20, offset=0, - category_id=None, exclude_category_ids=None, **kw): + category_id=None, exclude_category_ids=None, + apply_excluded=False, **kw): """ Search Odoo products by name or internal reference (SKU). @@ -61,6 +62,12 @@ class WooProductSearchController(http.Controller): if exclude_category_ids: domain.append(('categ_id', 'not in', [int(x) for x in exclude_category_ids])) + # Apply instance-level excluded categories + if apply_excluded and instance_id: + instance = request.env['woo.instance'].browse(int(instance_id)) + if instance.exists() and instance.excluded_category_ids: + domain.append(('categ_id', 'not in', instance.excluded_category_ids.ids)) + total = request.env['product.product'].search_count(domain) products = request.env['product.product'].search(domain, limit=limit, offset=offset) diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py index eabfeae2..617bfbb4 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py @@ -53,6 +53,10 @@ class WooInstance(models.Model): # Category mapping category_map_ids = fields.One2many('woo.category.map', 'instance_id', string='Category Mappings') + excluded_category_ids = fields.Many2many( + 'product.category', string='Hidden Categories', + help='Products in these categories will be hidden from the unmatched products list.' + ) # AI Configuration ai_provider = fields.Selection([ diff --git a/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv b/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv index 891ca552..5beb7f3f 100644 --- a/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv +++ b/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv @@ -28,3 +28,4 @@ access_woo_category_map_manager,woo.category.map.manager,model_woo_category_map, access_woo_setup_wizard_manager,woo.setup.wizard.manager,model_woo_setup_wizard,fusion_woocommerce.group_woo_manager,1,1,1,1 access_woo_product_fetch_manager,woo.product.fetch.manager,model_woo_product_fetch,fusion_woocommerce.group_woo_manager,1,1,1,1 access_woo_product_create_wizard_manager,woo.product.create.wizard.manager,model_woo_product_create_wizard,fusion_woocommerce.group_woo_manager,1,1,1,1 +access_woo_category_filter_manager,woo.category.filter.manager,model_woo_category_filter,group_woo_manager,1,1,1,1 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 f9100bef..ebccab50 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 @@ -61,9 +61,8 @@ export class ProductMapping extends Component { pageSize: 50, // Category filters - odooCategories: [], - odooFilterCategoryId: false, - odooExcludeCategoryIds: [], + categoryFilterActive: true, + excludedCategoryCount: 0, // Conflicts tab conflicts: [], @@ -71,7 +70,7 @@ export class ProductMapping extends Component { onWillStart(async () => { await this._loadInstances(); - await this._loadOdooCategories(); + await this._loadExcludedCategoryCount(); await this._refreshAll(); }); } @@ -131,12 +130,23 @@ export class ProductMapping extends Component { } } - async _loadOdooCategories() { + async _loadExcludedCategoryCount() { + if (!this.state.instanceId) { + this.state.excludedCategoryCount = 0; + return; + } try { - const result = await rpc("/woo/search/odoo_categories", {}); - this.state.odooCategories = result || []; + const result = await rpc("/web/dataset/call_kw", { + model: "woo.instance", + method: "read", + args: [[this.state.instanceId], ["excluded_category_ids"]], + kwargs: {}, + }); + if (result && result[0]) { + this.state.excludedCategoryCount = (result[0].excluded_category_ids || []).length; + } } catch (err) { - console.error("[ProductMapping] _loadOdooCategories error:", err); + console.error("[ProductMapping] _loadExcludedCategoryCount error:", err); } } @@ -150,11 +160,9 @@ 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); + // Pass excluded categories if filter is active + if (this.state.categoryFilterActive && this.state.instanceId) { + params.apply_excluded = true; } const result = await rpc("/woo/search/odoo_products", params); this.state.odooProducts = (result && result.results) || []; @@ -285,34 +293,28 @@ export class ProductMapping extends Component { // 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 openCategoryFilter() { + if (!this.state.instanceId) { + this.notification.add("Select an instance first.", { type: "warning" }); + return; } - } - - async applyExcludeCategories() { + await this.actionService.doAction({ + type: 'ir.actions.act_window', + res_model: 'woo.category.filter', + views: [[false, 'form']], + target: 'new', + context: { + default_instance_id: this.state.instanceId, + }, + }); + // Reload after wizard closes + await this._loadExcludedCategoryCount(); this.state.unmatchedOdooPage = 1; await this._loadOdooProducts(""); } - isCategoryExcluded(catId) { - return this.state.odooExcludeCategoryIds.includes(catId); - } - - async clearCategoryFilter() { - this.state.odooFilterCategoryId = false; - this.state.odooExcludeCategoryIds = []; + async toggleCategoryFilter() { + this.state.categoryFilterActive = !this.state.categoryFilterActive; this.state.unmatchedOdooPage = 1; await this._loadOdooProducts(""); } 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 f4bdbe0d..a434acef 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 @@ -336,18 +336,20 @@
Odoo Products
- - - + +
diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py b/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py index 67b9f9cb..286a6c1f 100644 --- a/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py @@ -1,3 +1,4 @@ from . import woo_setup_wizard from . import woo_product_fetch from . import woo_product_create +from . import woo_category_filter diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter.py b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter.py new file mode 100644 index 00000000..77922f92 --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter.py @@ -0,0 +1,42 @@ +from odoo import api, fields, models + + +class WooCategoryFilter(models.TransientModel): + _name = 'woo.category.filter' + _description = 'Manage Hidden Categories' + + instance_id = fields.Many2one('woo.instance', required=True) + category_ids = fields.Many2many( + 'product.category', string='Categories to Hide', + help='Select categories you want to hide from the unmatched products list.', + ) + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + instance_id = self.env.context.get('default_instance_id') + if instance_id: + instance = self.env['woo.instance'].browse(instance_id) + res['category_ids'] = [(6, 0, instance.excluded_category_ids.ids)] + return res + + def action_save(self): + """Save the hidden categories to the instance.""" + self.ensure_one() + self.instance_id.excluded_category_ids = [(6, 0, self.category_ids.ids)] + return {'type': 'ir.actions.act_window_close'} + + def action_clear_all(self): + """Remove all hidden categories.""" + self.ensure_one() + self.category_ids = [(5, 0, 0)] + return self._reopen() + + def _reopen(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'views': [(False, 'form')], + 'target': 'new', + } diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter_views.xml b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter_views.xml new file mode 100644 index 00000000..8deedd3a --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_category_filter_views.xml @@ -0,0 +1,30 @@ + + + + + woo.category.filter.form + woo.category.filter + +
+ + + + + + + + +
+
+
+ +