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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <p> tags.')
|
||||
prompt_long_description = fields.Text(string='Long Description Prompt',
|
||||
default='Write a detailed SEO-optimized product description in HTML format. Include sections with <h3> headings for Features, Specifications, and Benefits. Use <ul> 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()
|
||||
|
||||
Reference in New Issue
Block a user