# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) import logging from odoo import http from odoo.http import request _logger = logging.getLogger(__name__) class WooProductSearchController(http.Controller): """AJAX search endpoints used by the product mapping UI.""" # ------------------------------------------------------------------------- # Endpoints # ------------------------------------------------------------------------- @http.route( '/woo/search/odoo_products', 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, apply_excluded=False, **kw): """ Search Odoo products by name or internal reference (SKU). Params: query (str): Search string matched against name and default_code. 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 """ limit = min(int(limit or 20), 100) offset = int(offset or 0) domain = [] if query: domain = [ '|', ('name', 'ilike', query), ('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])) # 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)) # Search product.template to group variants together tmpl_domain = [] if query: tmpl_domain = [ '|', ('name', 'ilike', query), ('default_code', 'ilike', query), ] if category_id: tmpl_domain.append(('categ_id', '=', int(category_id))) if exclude_category_ids: if isinstance(exclude_category_ids, str): import json as _json2 try: exclude_category_ids = _json2.loads(exclude_category_ids) except (ValueError, TypeError): exclude_category_ids = [] if exclude_category_ids: tmpl_domain.append(('categ_id', 'not in', [int(x) for x in exclude_category_ids])) if apply_excluded and instance_id: instance = request.env['woo.instance'].browse(int(instance_id)) if instance.exists() and instance.excluded_category_ids: tmpl_domain.append(('categ_id', 'not in', instance.excluded_category_ids.ids)) total = request.env['product.template'].search_count(tmpl_domain) templates = request.env['product.template'].search(tmpl_domain, limit=limit, offset=offset) results = [] for tmpl in templates: variant_count = len(tmpl.product_variant_ids) # Use first variant as representative first_variant = tmpl.product_variant_ids[:1] results.append({ 'id': first_variant.id if first_variant else tmpl.id, 'template_id': tmpl.id, 'name': tmpl.name, 'default_code': tmpl.default_code or '', 'list_price': tmpl.list_price, 'qty_available': sum(tmpl.product_variant_ids.mapped('qty_available')), 'categ_name': tmpl.categ_id.name if tmpl.categ_id else '', 'variant_count': variant_count, 'has_variants': variant_count > 1, }) return { 'results': results, 'total': total, } @http.route( '/woo/search/woo_products', type='jsonrpc', auth='user', methods=['POST'], ) def search_woo_products(self, query='', instance_id=None, limit=20, offset=0, **kw): """ Search unmapped WooCommerce products from the woo.product.map model. Params: query (str): Search string matched against woo_product_name and woo_sku. instance_id (int): woo.instance ID — filters results to this instance. limit (int): Max results to return (default 20). offset (int): Offset for pagination (default 0). Returns: dict with 'results' list and 'total' count """ limit = min(int(limit or 20), 100) offset = int(offset or 0) domain = [('state', '=', 'unmapped')] if instance_id: domain.append(('instance_id', '=', int(instance_id))) if query: domain += [ '|', ('woo_product_name', 'ilike', query), ('woo_sku', 'ilike', query), ] total = request.env['woo.product.map'].search_count(domain) maps = request.env['woo.product.map'].search(domain, limit=limit, offset=offset) return { 'results': [ { 'id': m.id, 'woo_product_id': m.woo_product_id, 'woo_product_name': m.woo_product_name or '', 'woo_sku': m.woo_sku or '', 'woo_product_type': m.woo_product_type or '', 'woo_category_name': m.woo_category_name or '', } for m in maps ], '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'], ) def search_mapped(self, query='', instance_id=None, limit=20, offset=0, **kw): """ Search mapped WooCommerce ↔ Odoo product pairs. Params: query (str): Matched against woo_product_name, woo_sku, and linked product name. instance_id (int): woo.instance ID — filters results to this instance. limit (int): Max results to return (default 20). offset (int): Offset for pagination (default 0). Returns: dict with 'results' list and 'total' count """ limit = min(int(limit or 20), 100) offset = int(offset or 0) domain = [('state', '=', 'mapped')] if instance_id: domain.append(('instance_id', '=', int(instance_id))) if query: domain += [ '|', '|', ('woo_product_name', 'ilike', query), ('woo_sku', 'ilike', query), ('product_id.name', 'ilike', query), ] total = request.env['woo.product.map'].search_count(domain) maps = request.env['woo.product.map'].search(domain, limit=limit, offset=offset) return { 'results': [ { 'id': m.id, 'woo_product_id': m.woo_product_id, 'woo_product_name': m.woo_product_name or '', 'woo_sku': m.woo_sku or '', 'woo_product_type': m.woo_product_type or '', 'woo_permalink': m.woo_permalink or '', 'odoo_product_id': m.product_id.id if m.product_id else False, 'odoo_product_name': m.product_id.name if m.product_id else '', 'odoo_default_code': m.product_id.default_code or '' if m.product_id else '', 'odoo_price': m.product_id.list_price if m.product_id else 0.0, 'odoo_cost': m.product_id.standard_price if m.product_id else 0.0, 'woo_regular_price': m.woo_regular_price or 0.0, 'woo_sale_price': m.woo_sale_price or 0.0, 'sync_price': m.sync_price, 'sync_inventory': m.sync_inventory, 'instance_id': m.instance_id.id if m.instance_id else False, 'instance_name': m.instance_id.name if m.instance_id else '', 'is_variation': m.is_variation, 'odoo_variant_count': len(m.product_id.product_tmpl_id.product_variant_ids) if m.product_id else 0, 'wc_is_simple': (m.woo_product_type or 'simple') == 'simple' and not m.is_variation, 'needs_variant_push': ( m.product_id and not m.is_variation and (m.woo_product_type or 'simple') == 'simple' and len(m.product_id.product_tmpl_id.product_variant_ids) > 1 ), } for m in maps ], 'total': total, }