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 wc_categories = wc_product.get('categories', []) wc_cat_id = wc_categories[0].get('id', 0) if wc_categories else 0 wc_cat_name = wc_categories[0].get('name', '') if wc_categories else '' 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, 'woo_category_id': wc_cat_id, 'woo_category_name': wc_cat_name, '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', }