From 3493c439169185124ea3123424e3253261f18c6f Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 1 Apr 2026 14:30:02 -0400 Subject: [PATCH] feat: add category mapping model and AI settings on woo.instance Category mapping between Odoo product categories and WC categories with auto-match by name and manual mapping UI. AI settings for Claude/OpenAI with customizable prompts for product content generation. GPS coordinates for image geo-tagging pulled from company settings. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fusion_woocommerce/__manifest__.py | 1 + .../fusion_woocommerce/models/__init__.py | 1 + .../models/woo_category_map.py | 14 +++ .../fusion_woocommerce/models/woo_instance.py | 95 +++++++++++++++++++ .../security/ir.model.access.csv | 2 + .../views/woo_category_map_views.xml | 43 +++++++++ .../views/woo_instance_views.xml | 54 +++++++++++ 7 files changed, 210 insertions(+) create mode 100644 fusion-woo-odoo/fusion_woocommerce/models/woo_category_map.py create mode 100644 fusion-woo-odoo/fusion_woocommerce/views/woo_category_map_views.xml diff --git a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py index 1370bb1f..d015db9e 100644 --- a/fusion-woo-odoo/fusion_woocommerce/__manifest__.py +++ b/fusion-woo-odoo/fusion_woocommerce/__manifest__.py @@ -15,6 +15,7 @@ 'data/cron.xml', 'data/mail_template.xml', 'views/woo_instance_views.xml', + 'views/woo_category_map_views.xml', 'views/woo_product_map_views.xml', 'views/woo_order_views.xml', 'views/woo_sync_log_views.xml', diff --git a/fusion-woo-odoo/fusion_woocommerce/models/__init__.py b/fusion-woo-odoo/fusion_woocommerce/models/__init__.py index 0657c847..69a8d7ae 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/__init__.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/__init__.py @@ -1,5 +1,6 @@ from . import woo_shipping_carrier from . import woo_instance +from . import woo_category_map from . import woo_product_map from . import woo_order from . import woo_shipment diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_category_map.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_category_map.py new file mode 100644 index 00000000..89530f5e --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_category_map.py @@ -0,0 +1,14 @@ +from odoo import api, fields, models + + +class WooCategoryMap(models.Model): + _name = 'woo.category.map' + _description = 'WooCommerce Category Mapping' + _order = 'odoo_category_id' + + instance_id = fields.Many2one('woo.instance', required=True, ondelete='cascade') + odoo_category_id = fields.Many2one('product.category', string='Odoo Category') + woo_category_id = fields.Integer(string='WC Category ID', required=True) + woo_category_name = fields.Char(string='WC Category Name') + woo_category_slug = fields.Char(string='WC Category Slug') + company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) diff --git a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py index 08fb9480..a89e4445 100644 --- a/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py +++ b/fusion-woo-odoo/fusion_woocommerce/models/woo_instance.py @@ -51,6 +51,43 @@ class WooInstance(models.Model): customer_ids = fields.One2many('woo.customer', 'instance_id') sync_log_ids = fields.One2many('woo.sync.log', 'instance_id') + # Category mapping + category_map_ids = fields.One2many('woo.category.map', 'instance_id', string='Category Mappings') + + # AI Configuration + ai_provider = fields.Selection([ + ('claude', 'Claude (Anthropic)'), + ('openai', 'OpenAI'), + ], string='AI Provider') + ai_api_key = fields.Char(string='AI API Key', groups='base.group_system') + ai_model = fields.Char(string='AI Model', + help='e.g., claude-sonnet-4-5-20250514 for Claude or gpt-4o for OpenAI') + + # AI Prompts + prompt_product_title = fields.Text(string='Product Title Prompt', + default='Generate an SEO-optimized product title in Title Case. Keep it concise, include the brand name and key product features. Do not use ALL CAPS.') + prompt_short_description = fields.Text(string='Short Description Prompt', + default='Write a compelling 2-3 sentence product summary in HTML format. Highlight key benefits and features. Use

tags.') + prompt_long_description = fields.Text(string='Long Description Prompt', + default='Write a detailed SEO-optimized product description in HTML format. Include sections with

headings for Features, Specifications, and Benefits. Use
    lists for features. Make it informative and persuasive.') + prompt_meta_title = fields.Text(string='Meta Title Prompt', + default='Generate an SEO meta title under 60 characters. Include the primary keyword and brand name.') + prompt_meta_description = fields.Text(string='Meta Description Prompt', + default='Generate an SEO meta description under 160 characters. Include a call to action and primary keyword.') + prompt_image_alt = fields.Text(string='Image Alt Text Prompt', + default='Generate descriptive alt text for this product image. Be specific about the product shown. Keep under 125 characters.') + prompt_image_caption = fields.Text(string='Image Caption Prompt', + default='Generate a short image caption for this product photo. Include the product name and key visible feature.') + prompt_keywords = fields.Text(string='Keywords Prompt', + default='Generate 5-8 SEO focus keywords for this product, comma-separated. Include long-tail keywords.') + + # Company Info for Image Geo-tagging + geo_company_name = fields.Char(string='Company Name (Geo-tag)', compute='_compute_geo_info', store=False) + geo_company_address = fields.Char(string='Company Address (Geo-tag)', compute='_compute_geo_info', store=False) + geo_company_phone = fields.Char(string='Company Phone (Geo-tag)', compute='_compute_geo_info', store=False) + geo_lat = fields.Float(string='GPS Latitude', digits=(10, 7)) + geo_lng = fields.Float(string='GPS Longitude', digits=(10, 7)) + # Computed mapped_count = fields.Integer(compute='_compute_counts') unmapped_count = fields.Integer(compute='_compute_counts') @@ -72,6 +109,64 @@ class WooInstance(models.Model): ('create_date', '>=', yesterday), ]) + @api.depends('company_id') + def _compute_geo_info(self): + for rec in self: + company = rec.company_id + rec.geo_company_name = company.name or '' + rec.geo_company_address = ', '.join(filter(None, [ + company.street, + company.city, + company.state_id.name if company.state_id else '', + company.zip, + company.country_id.name if company.country_id else '', + ])) + rec.geo_company_phone = company.phone or '' + + def action_fetch_wc_categories(self): + """Fetch all WooCommerce categories and display for mapping.""" + self.ensure_one() + client = self._get_client() + CategoryMap = self.env['woo.category.map'] + page = 1 + fetched = 0 + while True: + try: + cats = client.get('products/categories', params={'page': page, 'per_page': 100}) + except Exception as e: + raise UserError('Failed to fetch WC categories: %s' % str(e)) + if not cats: + break + for wc_cat in cats: + wc_id = wc_cat['id'] + existing = CategoryMap.search([ + ('instance_id', '=', self.id), + ('woo_category_id', '=', wc_id), + ], limit=1) + if existing: + existing.write({ + 'woo_category_name': wc_cat.get('name', ''), + 'woo_category_slug': wc_cat.get('slug', ''), + }) + else: + # Try auto-match by name + odoo_cat = self.env['product.category'].search([ + ('name', '=ilike', wc_cat.get('name', '')), + ], limit=1) + CategoryMap.create({ + 'instance_id': self.id, + 'odoo_category_id': odoo_cat.id if odoo_cat else False, + 'woo_category_id': wc_id, + 'woo_category_name': wc_cat.get('name', ''), + 'woo_category_slug': wc_cat.get('slug', ''), + 'company_id': self.company_id.id, + }) + fetched += 1 + page += 1 + if len(cats) < 100: + break + return True + def _get_client(self): """Return a WooApiClient instance for this WooCommerce connection.""" self.ensure_one() 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 67653d82..8fe4bbde 100644 --- a/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv +++ b/fusion-woo-odoo/fusion_woocommerce/security/ir.model.access.csv @@ -23,5 +23,7 @@ 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_category_map_user,woo.category.map.user,model_woo_category_map,fusion_woocommerce.group_woo_user,1,0,0,0 +access_woo_category_map_manager,woo.category.map.manager,model_woo_category_map,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/views/woo_category_map_views.xml b/fusion-woo-odoo/fusion_woocommerce/views/woo_category_map_views.xml new file mode 100644 index 00000000..38da3da4 --- /dev/null +++ b/fusion-woo-odoo/fusion_woocommerce/views/woo_category_map_views.xml @@ -0,0 +1,43 @@ + + + + + + woo.category.map.list + woo.category.map + + + + + + + + + + + + + + woo.category.map.form + woo.category.map + +
    + + + + + + + + + + + + + + +
    +
    +
    + +
    diff --git a/fusion-woo-odoo/fusion_woocommerce/views/woo_instance_views.xml b/fusion-woo-odoo/fusion_woocommerce/views/woo_instance_views.xml index ffb89ac0..0df8abb6 100644 --- a/fusion-woo-odoo/fusion_woocommerce/views/woo_instance_views.xml +++ b/fusion-woo-odoo/fusion_woocommerce/views/woo_instance_views.xml @@ -133,6 +133,60 @@ + +
    +
    + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +