diff --git a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py index f63dca40..97726427 100644 --- a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py +++ b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py @@ -29,6 +29,8 @@ 'views/res_config_settings.xml', 'views/woo_dashboard.xml', 'views/woo_menus.xml', + 'wizard/woo_setup_wizard_views.xml', + 'wizard/woo_product_fetch_views.xml', ], 'assets': { 'web.assets_backend': [ 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 3186a8e4..67653d82 100644 --- a/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv +++ b/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv @@ -23,3 +23,5 @@ access_woo_return_user,woo.return.user,model_woo_return,fusion_woocommerce.group access_woo_return_manager,woo.return.manager,model_woo_return,fusion_woocommerce.group_woo_manager,1,1,1,1 access_woo_return_line_user,woo.return.line.user,model_woo_return_line,fusion_woocommerce.group_woo_user,1,0,0,0 access_woo_return_line_manager,woo.return.line.manager,model_woo_return_line,fusion_woocommerce.group_woo_manager,1,1,1,1 +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 diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py b/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py index d3e1bdcd..8c76722b 100644 --- a/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/__init__.py @@ -1 +1,2 @@ -# Wizards will be imported here +from . import woo_setup_wizard +from . import woo_product_fetch diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch.py b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch.py new file mode 100644 index 00000000..de64eeee --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch.py @@ -0,0 +1,171 @@ +import difflib +import logging + +from odoo import fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class WooProductFetch(models.TransientModel): + _name = 'woo.product.fetch' + _description = 'Fetch WooCommerce Products' + + instance_id = fields.Many2one('woo.instance', required=True, string='WooCommerce Instance') + state = fields.Selection([ + ('draft', 'Ready'), + ('fetching', 'Fetching...'), + ('matching', 'Matching...'), + ('done', 'Complete'), + ], default='draft') + total_fetched = fields.Integer(readonly=True) + auto_matched = fields.Integer(string='Auto-matched (SKU)', readonly=True) + suggested = fields.Integer(string='Name Suggestions', readonly=True) + unmatched = fields.Integer(readonly=True) + + def action_fetch(self): + self.ensure_one() + instance = self.instance_id + if not instance: + raise UserError("Please select a WooCommerce instance.") + + client = instance._get_client() + + # --- Fetch all products (paginated) --- + self.state = 'fetching' + all_products = [] + page = 1 + while True: + batch = client.get_products(page=page, per_page=100) + if not batch: + break + all_products.extend(batch) + if len(batch) < 100: + break + page += 1 + + # Expand variable products with their variations + expanded = [] + for product in all_products: + expanded.append(product) + if product.get('type') == 'variable': + var_page = 1 + while True: + variations = client.get_product_variations( + product['id'], page=var_page, per_page=100 + ) + if not variations: + break + for var in variations: + var['_parent_id'] = product['id'] + var['_is_variation'] = True + expanded.extend(variations) + if len(variations) < 100: + break + var_page += 1 + + total_fetched = len(expanded) + + # --- Match products --- + self.state = 'matching' + + ProductMap = self.env['woo.product.map'] + ProductProduct = self.env['product.product'] + + # Pre-load existing maps for this instance to avoid repeated queries + existing_woo_ids = set( + ProductMap.search([('instance_id', '=', instance.id)]).mapped('woo_product_id') + ) + + # Pre-load all odoo products with a SKU for fast lookup + odoo_products_with_sku = ProductProduct.search([('default_code', '!=', False)]) + sku_index = {p.default_code: p for p in odoo_products_with_sku} + + # For name matching, load all product names + all_odoo_products = ProductProduct.search([]) + odoo_name_list = [(p.name or '', p) for p in all_odoo_products] + + auto_matched = 0 + suggested_count = 0 + unmatched = 0 + + for wc_product in expanded: + woo_id = wc_product.get('id') + if not woo_id: + continue + + # Skip already-mapped + if woo_id in existing_woo_ids: + continue + + woo_sku = wc_product.get('sku') or '' + woo_name = wc_product.get('name') or '' + woo_type = wc_product.get('type', 'simple') + is_variation = wc_product.get('_is_variation', False) + parent_id = wc_product.get('_parent_id') or wc_product.get('parent_id') or 0 + + map_vals = { + 'instance_id': instance.id, + 'woo_product_id': woo_id, + 'woo_product_name': woo_name, + 'woo_sku': woo_sku, + 'woo_product_type': woo_type if not is_variation else 'variable', + 'is_variation': is_variation, + 'woo_parent_id': parent_id, + 'company_id': instance.company_id.id, + } + + # a) SKU match + matched_product = None + if woo_sku and woo_sku in sku_index: + matched_product = sku_index[woo_sku] + map_vals['product_id'] = matched_product.id + map_vals['state'] = 'mapped' + auto_matched += 1 + else: + # b) Name similarity + if woo_name and odoo_name_list: + ratios = [ + (difflib.SequenceMatcher(None, woo_name.lower(), name.lower()).ratio(), p) + for name, p in odoo_name_list + ] + best_ratio, best_product = max(ratios, key=lambda x: x[0]) + if best_ratio > 0.8: + map_vals['product_id'] = best_product.id + map_vals['state'] = 'unmapped' + suggested_count += 1 + else: + map_vals['state'] = 'unmapped' + unmatched += 1 + else: + map_vals['state'] = 'unmapped' + unmatched += 1 + + ProductMap.create(map_vals) + + self.write({ + 'total_fetched': total_fetched, + 'auto_matched': auto_matched, + 'suggested': suggested_count, + 'unmatched': unmatched, + 'state': 'done', + }) + + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } + + def action_open_mapping(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Product Mapping', + 'res_model': 'woo.product.map', + 'view_mode': 'list,form', + 'domain': [('instance_id', '=', self.instance_id.id)], + 'target': 'current', + } diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch_views.xml b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch_views.xml new file mode 100644 index 00000000..83add641 --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_product_fetch_views.xml @@ -0,0 +1,61 @@ + + + + + woo.product.fetch.form + woo.product.fetch + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Fetch WooCommerce Products + woo.product.fetch + form + new + + +
diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_setup_wizard.py b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_setup_wizard.py new file mode 100644 index 00000000..7d89b834 --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_setup_wizard.py @@ -0,0 +1,145 @@ +import secrets +import logging + +from odoo import api, fields, models +from odoo.exceptions import UserError + +from ..lib.woo_api_client import WooApiClient + +_logger = logging.getLogger(__name__) + + +class WooSetupWizard(models.TransientModel): + _name = 'woo.setup.wizard' + _description = 'WooCommerce Setup Wizard' + + # Step 1: Connection + name = fields.Char(string='Instance Name', required=True) + url = fields.Char(string='WooCommerce URL', required=True) + consumer_key = fields.Char(string='Consumer Key', required=True) + consumer_secret = fields.Char(string='Consumer Secret', required=True) + + # Step 2: Configuration + default_warehouse_id = fields.Many2one('stock.warehouse', string='Default Warehouse') + sync_interval = fields.Selection([ + ('5', '5 Minutes'), + ('15', '15 Minutes'), + ('30', '30 Minutes'), + ('60', '1 Hour'), + ], string='Sync Interval', default='15') + + # State tracking + step = fields.Selection([ + ('connection', 'Connection'), + ('config', 'Configuration'), + ('done', 'Done'), + ], default='connection') + connection_tested = fields.Boolean() + instance_id = fields.Many2one('woo.instance') + api_key = fields.Char(string='Generated API Key', readonly=True) + + def action_test_connection(self): + self.ensure_one() + try: + client = WooApiClient( + url=self.url, + consumer_key=self.consumer_key, + consumer_secret=self.consumer_secret, + ) + success, info = client.test_connection() + if success: + self.connection_tested = True + else: + raise UserError(f"Connection failed: {info}") + except UserError: + raise + except Exception as exc: + raise UserError(f"Connection error: {exc}") from exc + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } + + def action_next_step(self): + self.ensure_one() + if self.step == 'connection': + if not self.connection_tested: + raise UserError("Please test the connection before proceeding.") + self.step = 'config' + elif self.step == 'config': + self.step = 'done' + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } + + def action_back(self): + self.ensure_one() + if self.step == 'config': + self.step = 'connection' + elif self.step == 'done': + self.step = 'config' + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } + + def action_complete(self): + self.ensure_one() + api_key = secrets.token_urlsafe(32) + instance = self.env['woo.instance'].create({ + 'name': self.name, + 'url': self.url, + 'consumer_key': self.consumer_key, + 'consumer_secret': self.consumer_secret, + 'default_warehouse_id': self.default_warehouse_id.id, + 'sync_interval': self.sync_interval, + 'odoo_api_key': api_key, + 'state': 'connected', + }) + self.instance_id = instance + self.api_key = api_key + self.step = 'done' + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } + + def action_open_instance(self): + self.ensure_one() + if not self.instance_id: + raise UserError("No instance created yet.") + return { + 'type': 'ir.actions.act_window', + 'res_model': 'woo.instance', + 'res_id': self.instance_id.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_fetch_products(self): + self.ensure_one() + if not self.instance_id: + raise UserError("No instance created yet.") + wizard = self.env['woo.product.fetch'].create({ + 'instance_id': self.instance_id.id, + }) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'woo.product.fetch', + 'res_id': wizard.id, + 'view_mode': 'form', + 'target': 'new', + } diff --git a/fusion-woo-odoo/fusion_woocommerce/wizard/woo_setup_wizard_views.xml b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_setup_wizard_views.xml new file mode 100644 index 00000000..28747214 --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/wizard/woo_setup_wizard_views.xml @@ -0,0 +1,130 @@ + + + + + woo.setup.wizard.form + woo.setup.wizard + +
+ + +
+ + + Step 1: Connection + + + Step 1: Connection ✓ + + + Step 2: Configuration + + + Step 2: Configuration ✓ + + + Step 2: Configuration + + + Step 3: Done + + + Step 3: Done + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+ + + WooCommerce Setup Wizard + woo.setup.wizard + form + new + + +